ESLint
Juniper schemas are meant to be built once, at module scope, and reused. The juniper/eslint plugin reports schemas built anywhere else.
// eslint.config.js
import juniper from 'juniper/eslint';
export default [juniper.configs.recommended];configs.recommended registers the plugin (as juniper) and turns on every rule as an error. To pick rules yourself, register it and configure them:
// eslint.config.js
import juniper from 'juniper/eslint';
export default [
{
plugins: { juniper },
rules: { 'juniper/top-level-schema': 'warn' },
},
];The plugin is a separate entry point, so importing juniper never loads it. eslint is an optional peer dependency, needed only when you use the plugin.
juniper/top-level-schema
Reports a schema built inside a function, or in a class instance field, rather than once at module scope:
import { objectSchema, stringSchema } from 'juniper';
// ❌ built on every call
export const slowJson = () => objectSchema({ properties: { name: stringSchema() } }).toJSON();
// ✅ built once
const bodySchema = objectSchema({ properties: { name: stringSchema() } });
export const fastJson = () => bodySchema.toJSON();Every builder call clones the schema, so a schema built per call is wasted work, often on a hot path like a request handler. With .define(), it's worse: toJSON() throws when two different instances are defined under the same name, as each call's would be.
What it checks:
- Calls to the schema factories imported from
juniper:objectSchema(),stringSchema()and the rest,ObjectSchema.create()and the other classes'create(), anddefineRecursive(). Named, renamed and namespace imports are all followed. - Inside any function, and in non-static class fields, which run once per instance. Static fields run once, so they're fine.
- Schemas built in a
defineRecursivecallback are fine too, since it runs once, when the definition is built. - So are schemas built in a function that's called immediately at module scope, like
const a = (() => objectSchema())();, since it also runs once.
What it doesn't check:
- Methods called on an existing schema, like
.toJSON(),.pick()or.nullable(). Those need type information to tell apart from other methods. - Whether a function only runs at module scope. A helper that builds a schema from its arguments, like
const titled = title => objectSchema({ title }), is reported, even if it's only called at the top level. Disable the rule for that helper, with a comment saying why:
import { objectSchema } from 'juniper';
// eslint-disable-next-line juniper/top-level-schema -- only called at module scope
const titled = (title: string) => objectSchema({ title });
export const user = titled('User');There is no autofix: moving a schema out of its function can change what it refers to.