Guide for AI coding agents (Claude Code and others) working in this repository. This is the source of truth — CLAUDE.md points here.
@phpdevsr/js-validation — lightweight, zero-dependency vanilla JavaScript form validation library inspired by jquery-validation. Ships as UMD + ES module, no runtime deps, Node >= 22.
npm ci # install (lockfile-respecting)
npm run build # vite build (UMD + ESM, unminified) then minified build
npm test # vitest run — unit tests
npm run test:watch # vitest watch
npm run test:e2e # playwright (chromium + firefox + webkit), auto-serves on :5174
npm run release # maintainer-only: bumps version + changelog (.github/scripts/release.sh)CI uses Node 24 (.github/workflows/ci.yml), .nvmrc says 22, package.json engines says >=22. Use 22+ locally.
src/core.js—VanillaValidatorclass. The whole engine: rule registration (addMethod), locale messages (addLocaleMessages), rule resolution fromoptions.rules+data-rule-*dataset, per-fieldelement()validation,validate(),showError/clearError, submit + input event binding.src/index.js— full bundle entry: imports core, all rules, registers all locales, exportsjsValidationdefault +VanillaValidatornamed.src/core-entry.js— core-only entry (no rules, no locales). For selective imports viajs-validation/core+js-validation/rules/*.src/rules/*.js— one file per built-in rule. Each callsVanillaValidator.addMethod(name, validateFn, message).src/locales.js— aggregates locale files fromsrc/locales/<lang>.js(currentlyen,es).test/js-validation.test.js— vitest unit tests (jsdom-free; hand-rolled fake field/form objects).test/selective-import.test.js— verifies core-only entry works.test/e2e/<rule>/— Playwright fixture (fixture.html) + spec per rule;test/e2e/validation.spec.jsis the general one.vite.config.js(unminified),vite.config.min.js(terser-minified),vite.config.e2e.js(dev server for e2e, roottest/e2e, aliases/src→src).docs-site/— separate Astro docs site (ownpackage.json).dist/— build output, gitignored.
Rule registration is global/static. VanillaValidator.methods and .locales are static class properties. Rules register themselves as a side effect of import — src/rules/email.js just calls addMethod at module top level. Importing a rule file = registering that rule. This is why core-entry.js + selective rule imports work, and why test file load order matters (a rule imported by one test file is registered globally for all).
Adding a built-in rule (see CONTRIBUTING.md):
- Create
src/rules/myRule.js—import { VanillaValidator } from '../core.js'thenaddMethod(name, (value, param, field, validator) => bool, message). Message can be a string or(param, field) => string. - Import it in
src/index.js(import './rules/myRule.js'). - Add a locale entry in each
src/locales/<lang>.js. - Add tests in
test/js-validation.test.jsand an e2e fixture/spec intest/e2e/.
Adding a locale: create src/locales/<lang>.js exporting a { ruleName: message } object, then re-export it from src/locales.js.
Rule resolution per field (_rulesFor): merges data-rule-* dataset attributes with options.rules[fieldName]. Dataset keys like dataRuleEmail → rule name email (first char after rule lowercased). A data-rule-* with empty value means true; false/'false' param skips the rule. range params are JSON-parsed arrays.
Message resolution (_messageFor): options.messages[field][rule] → locale messages for this.lang (default en) → English fallback → rule's default message. Functions receive (param, field).
Validation runs synchronously in element(): iterates rules in insertion order, fails on first invalid rule, shows error, returns false. validate() runs element() over all non-disabled, named, non-submit fields.
Error display toggles an is-invalid class on the field and inserts an error element (<span class="invalid-feedback">, configurable via options) after the field, keyed by data-jsv-error-for="<name>". The unit tests use fake DOM objects with a manual classList Set, so no jsdom is needed.
- ESM throughout (
"type": "module"). No build step for source — Vite only bundlessrc/index.jsfor dist. - Vanilla JS, no TypeScript, no transpilation. Keep source readable as-is.
- Zero runtime dependencies — do not add any. Dev deps only (vite, vitest, playwright, terser).
- Match existing rule file style: single
addMethodcall, message as string or(param) =>fn. - Prototype-pollution hardening:
addMethod/addLocaleMessagesreject__proto__/constructor/prototypekeys. Preserve this guard when touching registration code.
- Branching:
developis the working branch (PRs target it);mainis release-only. Current default for PRs isdevelop. - PR titles must follow Conventional Commits:
feat|fix|chore|docs|perf|refactor|style|test|config|revert(scope)?!: summary(enforced by.github/prlint.json). - Dependabot keeps GitHub Actions and dev deps bumped — expect frequent
chore(deps)commits. - Don't commit
dist/; it's built in CI and gitignored.