Skip to content

feat: precompile a schema into a check for validate - #231

Merged
alexander-akait merged 3 commits into
mainfrom
feat/precompile
Oct 17, 2026
Merged

alexander-akait merged 3 commits into
mainfrom
feat/precompile

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Compiling a schema at runtime loads ajv. That costs about 150 ms for the first schema of a process, plus a few to tens of milliseconds for each later one, and every loader and plugin that validates its options pays it on every start.

This adds:

  • schema-utils precompile [--check] <schema.json>...: compiles a schema ahead of time into <schema>.check.js next to it. --check writes nothing and fails when a check is missing or out of date, for CI.
  • A check fourth argument to validate(schema, options, configuration, check): a valid value is only checked and ajv is never loaded. An invalid value still runs the full validation, so the error messages are unchanged.
  • precompile(schema, { loadSchema }): the same as an API. It returns the module source and is loaded lazily from the main entry.
validate(schema, options, { name: "CSS Loader" }, require("./options.check.js"));

Details

  • instanceof, absolutePath and undefinedAsNull are compiled with functions at runtime, which ajv standalone can't emit. They get code-generating twins with the same semantics. undefinedAsNull also sets undefined to null in the parent, as the full validation does.
  • The generated module loads the ajv runtime helpers it needs (equal, ucs2length, uri, formats) through schema-utils/dist/runtime. The packages shipping a check depend on schema-utils, not on ajv. Each helper is a getter, so only the ones a schema uses get loaded.
  • Annotations (title, description, link, …) are stripped before compiling. The file: URLs of the compiled and loaded schemas are made relative, so the output is the same on every machine. A file: URL that is a schema value is left alone.
  • The output starts with /* eslint-disable */ and // @ts-nocheck so consumers don't lint or type-check generated code. It is CommonJS.

Measurements

Node 22, fresh process, packed tarball installed into a scratch project, css-loader's options.json:

validate(schema, options) validate(schema, options, config, check)
First validation ~150–160 ms ~8 ms
Error message for invalid options the full one identical
ajv core loaded for valid options yes no

For css-loader, style-loader and minimizer-webpack-plugin together: ~280 ms with ajv at runtime against ~12 ms precompiled.

Tests

  • Every pass and fail case in test/index.test.js (about 400 cases, including the webpack schema) also runs the precompiled check and must agree. Pass cases must also leave the same value behind, which covers undefinedAsNull.
  • New tests in test/precompile.test.js cover validate with a check, the keywords, external $ref, path relativizing, the runtime helpers and the CLI.
  • Outside the suite, I compiled all of webpack's schemas/**/*.json with the CLI. The resulting WebpackOptions check agrees with webpack's committed one on all 3,698 configs from webpack's configCases, watchCases and hotCases.

Follow-ups (separate PRs)

  • webpack: this.getOptions(schema, check) for loaders, and switching tooling/precompile-schemas.js to this command.
  • eslint-config-webpack: a webpack/require-precompiled-schema rule.
  • Precompiling in minimizer-webpack-plugin and the webpack-contrib loaders and plugins.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QYPkbvwyRWt2SHgXDoYRdo


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added command-line and programmatic schema precompilation to generate standalone checks that report whether values are valid.
    • validate accepts an optional precompiled check, avoiding schema compilation when values pass and retaining standard validation errors when they don’t.
    • Added a CLI check mode that flags missing or outdated generated checks. Precompilation supports external schema references, including custom loading options. The CLI can process multiple schemas and reports failures for individual files.

Compiling a schema at runtime loads `ajv`, about 150ms for the first
schema of a process and a few to tens of milliseconds for every other one,
which every loader and plugin validating its options pays on every start.

`schema-utils precompile <schema.json>` compiles a schema ahead of time
into `<schema>.check.js`, and `validate` takes it as its fourth argument.
A valid value is only checked, `ajv` is loaded to report the errors of an
invalid one, so they stay the same. `--check` fails when a check is
missing or out of date, for CI.

The keywords `validate` compiles with a function (`instanceof`,
`absolutePath`, `undefinedAsNull`) get code twins with the same semantics,
the generated module loads the `ajv` runtime helpers it needs through
`schema-utils`, which the package depends on, and the paths of the schemas
are relative so the output is the same on every machine. Every case of the
validation tests runs the precompiled check too and has to agree with it.
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.41860% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.11%. Comparing base (c9e4796) to head (5cfcea2).

Files with missing lines Patch % Lines
src/precompile.js 99.03% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #231      +/-   ##
==========================================
+ Coverage   96.72%   97.11%   +0.38%     
==========================================
  Files          10       13       +3     
  Lines        1009     1180     +171     
  Branches      415      457      +42     
==========================================
+ Hits          976     1146     +170     
- Misses         31       32       +1     
  Partials        2        2              
Flag Coverage Δ
integration 97.11% <99.41%> (+0.38%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e3f282de-0256-4f67-b933-b2649f9202e1
📥 Commits

Reviewing files that changed from the base of the PR and between 149a247 and 5ddcefb.

📒 Files selected for processing: 1
  • bin/schema-utils.js
🚧 Files skipped from review as they are similar to previous changes: 1
  • bin/schema-utils.js

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Walkthrough

Adds an API to compile schemas into standalone CommonJS checks and an optional check parameter to validate. Adds a CLI to generate checks or report missing and outdated files. The precompiler supports file-based references by default and accepts a custom loader for other external references. Tests cover generated checks, validation behavior, and CLI commands.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 5ddce

Failed precompilation now returns a nonzero exit status, so the selected change is ready to merge after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: adding schema precompilation that produces a check for validate.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 15 files.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches: 💡 1
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cfb06bdb-e125-4bbf-a633-73ea834668d3
📥 Commits

Reviewing files that changed from the base of the PR and between c9e4796 and 149a247.

📒 Files selected for processing: 18
  • .changeset/precompiled-schemas.md
  • README.md
  • bin/schema-utils.js
  • declarations/cli.d.ts
  • declarations/index.d.ts
  • declarations/precompile.d.ts
  • declarations/runtime.d.ts
  • declarations/validate.d.ts
  • eslint.config.mjs
  • package.json
  • src/cli.js
  • src/index.js
  • src/precompile.js
  • src/runtime.js
  • src/validate.js
  • test/helpers/precompiled.js
  • test/index.test.js
  • test/precompile.test.js

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Comment thread bin/schema-utils.js Outdated
Before Node.js 15 an unhandled rejection only warns and the process exits
with 0, so a check that could not be written passed in CI.
Fixes GHSA-hrr3-gc8f-f4qj, which fails `npm audit` in the lint job.
@alexander-akait
alexander-akait merged commit 576fa35 into main Oct 17, 2026
52 of 61 checks passed
@alexander-akait
alexander-akait deleted the feat/precompile branch October 17, 2026 13:02
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.

1 participant