Skip to content

feat(core): add a @jsonforms/core/core-only entry point that never loads Ajv - #1

Open
andrewperry wants to merge 7 commits into
masterfrom
feat/core-only-entry
Open

andrewperry wants to merge 7 commits into
masterfrom
feat/core-only-entry

Conversation

@andrewperry

Copy link
Copy Markdown

Refs eclipsesource#1557, following the discussion there.

Stacked on eclipsesource#2644, eclipsesource#2645 and eclipsesource#2648 (and eclipsesource#2643, which eclipsesource#2645 includes). Until those are merged this PR shows their commits too; only the last three commits are new here:

  • feat(core): add @jsonforms/core/core-only entry point that never loads Ajv
  • test(vue-vuetify): update snapshots now that clearAllIds resets the ids in use
  • test(vue-vuetify): unmount mounted forms after each test

I'll rebase onto master once the stack lands, and mark this ready for review then.

What changed

  • New entry point @jsonforms/core/core-only. It exports everything @jsonforms/core does except createAjv, createAjvValidator and validate, and never loads ajv or ajv-formats.
  • @jsonforms/core is unchanged: same exports, Ajv still the default. The bindings and renderer sets are untouched.
  • Ajv lives in one module. util/validator.ts now also holds createAjvValidator (moved from util/formValidator.ts). It is the only module importing Ajv at runtime, and when loaded it registers itself as the default through the new util/ajvDefault.ts. The reducer asks createDefaultAjv() instead of importing createAjv. The util barrel no longer re-exports validator.ts; the main entry point does.
  • Without Ajv, init / updateCore need a validator, an ajv instance or validationMode: 'NoValidation', and otherwise throw an error naming those options.
  • Build. Rollup builds both entry points with a shared chunk (lib/jsonforms-core-shared.{esm,cjs}.js), so importing both doesn't duplicate core. The main / module / typings paths are unchanged and no exports map is added, so deep imports keep working. The subpath resolves through core-only/package.json, which is added to files.
  • Docs. The core README has a "Using JSON Forms without Ajv" section.

Vue Vuetify test changes

Snapshots (ID numbers only). 20 snapshots now show # where they showed #6 / #8. Under Vitest, core's CommonJS build was evaluated twice (by the module runner and by Node's require), so clearAllIds() in the specs reset a different copy of the ID state from the one the renderers used. With the shared chunk, both copies of the small entry file require the same chunk, so clearAllIds() works as the specs intend. The diff is 96 lines each way, identical once #<n> is replaced by #.

Unmount after each test. mountJsonForms() attaches to document.body and nothing unmounted the forms. The duplicate core copy had hidden this by keeping IDs unique; now the next form reused the same IDs while the previous one was still in the document. tests/setup.ts enables Vue Test Utils' enableAutoUnmount(afterEach). In the Time spec, 0 to 6 leftover forms were in the document before each test without it, and 0 with it. Snapshots are unchanged.

Tests

  • test/coreOnly.test.ts makes requiring ajv / ajv-formats throw, then loads the entry point. It covers:

    • the Ajv helpers being absent;
    • validating through a Form Validator with no Ajv instance;
    • using an ajv instance passed in the options;
    • NoValidation;
    • init without a validator throwing;
    • schema-based rules evaluating.

    If the entry point imports validator.ts, all six fail with 'ajv' was loaded by @jsonforms/core/core-only.

  • test/defaultAjv.test.ts: @jsonforms/core still creates and uses a default Ajv instance and exports the Ajv helpers.

  • Core: 538 passing, plus the existing known failure. Lint and Prettier are clean, and all packages build.

  • Other packages, all passing: React, Vanilla, Material (465), Vue, Vue Vanilla (93), Vue Vuetify (111), Angular and Angular Material.

Bundle size

An app using core directly with a Form Validator (@cfworker/json-schema) and its own Lit renderers, measured with esbuild, minified, gzip -9:

Import min gzip
@jsonforms/core 566.2 kB 136.0 kB
@jsonforms/core/core-only 436.3 kB 96.4 kB

The core-only bundle contains no ajv or ajv-formats modules. It also runs under a CSP without 'unsafe-eval' with no violations.

Open question

With no validator configured, the core-only entry point throws rather than silently skipping validation, so forms relying on required checks can't lose them unnoticed. Happy to change that if you'd prefer otherwise.

gigibiffi84 and others added 7 commits October 18, 2026 18:34
Adds a `validator` init/updateCore option taking a factory
`(schema) => FormValidator`. Core calls it whenever the schema changes
and stores the result in `state.validator`; without the option, Ajv is
the default Form Validator via `createAjvValidator`. Like the other
options, `validator: undefined` keeps the current value.

- `FormValidator.validate(data)` returns `ValidationIssue`s, which
  `issuesToErrors` converts to the Ajv error objects stored in the
  state. A missing `key` becomes `custom`; a `required` issue addressed
  to the missing property is split into parent path plus
  `params.missingProperty`. Ajv errors round-trip unchanged.
- `compiledAjvValidator(validateFn)` wraps an already compiled Ajv
  function, e.g. generated via Ajv standalone code.
- `getValidator(state)` exposes the current Form Validator.
- Switching to NoValidation via setValidationMode now also drops the
  validator, so later data updates no longer reintroduce errors.

`JsonFormsCore.validator` changes from the compiled Ajv function to a
Form Validator, see MIGRATION.md.

Part of eclipsesource#1498

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B36yeVG6EUUhwMCEzybGEH
…ing them with Ajv

Combinator tab selection compiled every oneOf/anyOf/allOf branch with
Ajv and then ignored all errors except the structural keywords
required, additionalProperties, type, enum and const. The new
isStructuralMatch evaluates exactly those keywords itself, recursively
through properties, patternProperties, items, $ref, nested combinators
and if/then/else.

Tab selection therefore no longer depends on an Ajv instance or on
code generation, so it also works under a Content Security Policy
without unsafe-eval. Equivalence with the previous filtered-Ajv
behaviour is covered by tests comparing both approaches keyword by
keyword.

Part of eclipsesource#1498

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Schema based rule conditions were always evaluated with
ajv.validate(condition.schema, value), the last place where a custom
Form Validator did not apply.

- Widen the `ajv` parameter of isVisible, isEnabled, isReadonly, the
  eval* functions and Runtime.isVisible/isEnabled to RuleValidator: an
  Ajv instance (unchanged behaviour), a FormValidatorFactory (one Form
  Validator per condition schema, cached) or undefined (the Structural
  Matcher).
- Add matchesConditionSchema and the getRuleValidator selector
  (`validatorFactory`, otherwise `ajv`); core's own mappers pass
  getRuleValidator(state) instead of getAjv(state).

Part of eclipsesource#1498

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B36yeVG6EUUhwMCEzybGEH
While a `validator` factory is configured and no `ajv` is passed, INIT
and UPDATE_CORE no longer create a default Ajv instance, so `core.ajv`
stays undefined and nothing compiles schemas with an instance nobody
configured. getOrCreateAjv gains an optional `createDefault` parameter;
an explicitly passed `ajv` is still stored. vue-vuetify's
AdditionalProperties reads Ajv optionally.

Part of eclipsesource#1498

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B36yeVG6EUUhwMCEzybGEH
…s Ajv

Adopters who use core directly with a Form Validator still bundle Ajv and
ajv-formats (about 40 kB gzip), because core imports them for its
fallback createAjv(). The new opt-in entry point exports everything
@jsonforms/core does except createAjv, createAjvValidator and validate,
and never loads either package. @jsonforms/core itself is unchanged.

Ajv now lives in util/validator.ts, which also holds createAjvValidator
and registers itself as the default when loaded (util/ajvDefault.ts).
The reducer asks for that default instead of importing createAjv. Without
Ajv, init and updateCore need a validator, an ajv instance or
validationMode 'NoValidation', and throw otherwise.

Rollup builds both entry points with a shared chunk, so importing both
does not duplicate core. main, module and typings are unchanged and no
exports map is added; the subpath resolves through core-only/package.json.

Refs eclipsesource#1557
…ds in use

The specs call clearAllIds() before each mount so that snapshots always
get the same ids. Under Vitest, core's CommonJS build used to be
evaluated twice (by the module runner and by Node's require), so
clearAllIds() reset one copy of the id state while the renderers
allocated from the other, and ids grew across tests (eclipsesource#6, eclipsesource#8, ...).

With core's code in a shared chunk, both copies of the entry file
require the same chunk, so there is one id state and the first control
gets '#' as intended. Only id numbers change: 96 lines each way, identical
once '#<n>' is replaced by '#'.
mountJsonForms() attaches to document.body and nothing unmounted the
forms, so every test left its form in the document. Now that
clearAllIds() resets the ids in use, the next form reuses the same ids,
and the document briefly held duplicates (e.g. two id="#-input").

Enable Vue Test Utils' auto-unmount in the shared setup so each test
starts with an empty document. Snapshots are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants