Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/precompiled-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"schema-utils": minor
---

perf: add `schema-utils precompile` to compile a schema ahead of time into a check that `validate` takes as its fourth argument, so valid options are validated without loading `ajv`
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,37 @@ class Plugin {
export default Plugin;
```

### Precompiled schemas

Compiling a schema at runtime loads `ajv`, which costs about 150ms for the first schema of a
process and a few to tens of milliseconds for every other one. A loader or a plugin can compile its
schema ahead of time instead, into a check that `validate` takes as its fourth argument:

```console
npx schema-utils precompile src/options.json
```

This writes `src/options.check.js` next to the schema. Commit it, and publish it with the package:

```js
import { validate } from "schema-utils";
import check from "./options.check.js";
import schema from "./options.json";

validate(schema, options, { name: "Loader Name" }, check);
```

When the options are valid, the check is all that runs and `ajv` is never loaded. When they are
not, `validate` runs the full validation to report what is wrong, so the errors are the same as
without a check.

Run `schema-utils precompile --check src/options.json` in CI to fail when the check is missing or
out of date with its schema. The generated module needs `schema-utils` as a dependency of the
package and is CommonJS.

A relative `$ref` to another file resolves against the schema. To compile a schema from code, use
`precompile(schema, { loadSchema })`, it returns the source of the module.

### Allow to disable and enable validation (the `validate` function do nothing)

This can be useful when you don't want to do validation for `production` builds.
Expand Down
16 changes: 16 additions & 0 deletions bin/schema-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env node

/* eslint-disable no-console */

require("../dist/cli")
.default(process.argv.slice(2), { log: console.log, error: console.error })
.then(
(exitCode) => {
process.exitCode = exitCode;
},
(error) => {
// Before Node.js 15 an unhandled rejection exits with 0
console.error(error);
process.exitCode = 1;
},
);
23 changes: 23 additions & 0 deletions declarations/cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export default cli;
export type CliIO = {
/**
* prints a message
*/
log: (message: string) => void;
/**
* prints an error
*/
error: (message: string) => void;
};
/**
* @typedef {object} CliIO
* @property {(message: string) => void} log prints a message
* @property {(message: string) => void} error prints an error
*/
/**
* Runs the command line interface.
* @param {string[]} args arguments without `node` and the script
* @param {CliIO} io where the output goes
* @returns {Promise<number>} exit code
*/
declare function cli(args: string[], io: CliIO): Promise<number>;
10 changes: 10 additions & 0 deletions declarations/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ export type JSONSchema7 = import("./validate").JSONSchema7;
export type ExtendedSchema = import("./validate").ExtendedSchema;
export type ValidationErrorConfiguration =
import("./validate").ValidationErrorConfiguration;
export type PrecompiledCheck = import("./validate").PrecompiledCheck;
export type PrecompileOptions = import("./precompile").PrecompileOptions;
import { ValidationError } from "./validate";
import { disableValidation } from "./validate";
import { enableValidation } from "./validate";
import { needValidate } from "./validate";
/**
* @param {Parameters<typeof import("./precompile").default>} args arguments
* @returns {ReturnType<typeof import("./precompile").default>} source of the module
*/
export function precompile(
schema: import("./validate").Schema,
options?: import("./precompile").PrecompileOptions | undefined,
): ReturnType<typeof import("./precompile").default>;
import { validate } from "./validate";
export {
ValidationError,
Expand Down
26 changes: 26 additions & 0 deletions declarations/precompile.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default precompile;
export type PrecompileOptions = {
/**
* loads a schema an external `$ref` points to
*/
loadSchema?: ((uri: string) => Promise<AnySchemaObject>) | undefined;
};
export type Ajv = import("ajv").default;
export type AnySchemaObject = import("ajv").AnySchemaObject;
export type KeywordCxt = import("ajv").KeywordCxt;
export type Schema = import("./validate").Schema;
export type EXPECTED_ANY = any;
/**
* @typedef {object} PrecompileOptions
* @property {((uri: string) => Promise<AnySchemaObject>)=} loadSchema loads a schema an external `$ref` points to
*/
/**
* Compiles a schema into the source of a CommonJS module that exports a check for it.
* @param {Schema} schema schema
* @param {PrecompileOptions=} options options
* @returns {Promise<string>} source of the module
*/
declare function precompile(
schema: Schema,
options?: PrecompileOptions | undefined,
): Promise<string>;
31 changes: 31 additions & 0 deletions declarations/runtime.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export = runtime;
/**
* @typedef {object} Runtime
* @property {(a: any, b: any) => boolean} equal deep equality for `const`, `enum` and `uniqueItems`
* @property {(str: string) => number} ucs2length string length in code points for `minLength` and `maxLength`
* @property {any} uri URI parser for `$ref` to an `$id`
* @property {Record<string, any>} formats formats of `ajv-formats` for `format`
*/
/** @type {Runtime} */
declare const runtime: Runtime;
declare namespace runtime {
export { Runtime };
}
type Runtime = {
/**
* deep equality for `const`, `enum` and `uniqueItems`
*/
equal: (a: any, b: any) => boolean;
/**
* string length in code points for `minLength` and `maxLength`
*/
ucs2length: (str: string) => number;
/**
* URI parser for `$ref` to an `$id`
*/
uri: any;
/**
* formats of `ajv-formats` for `format`
*/
formats: Record<string, any>;
};
13 changes: 13 additions & 0 deletions declarations/validate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type JSONSchema4 = import("json-schema").JSONSchema4;
export type JSONSchema6 = import("json-schema").JSONSchema6;
export type JSONSchema7 = import("json-schema").JSONSchema7;
export type ErrorObject = import("ajv").ErrorObject;
export type EXPECTED_ANY = any;
export type ExtendedSchema = {
/**
* format minimum
Expand Down Expand Up @@ -74,6 +75,10 @@ export type ErrorPathNode = {
*/
size: number;
};
/**
* A check precompiled from a schema with `precompile`, it only says whether the value is valid.
*/
export type PrecompiledCheck = (value: EXPECTED_ANY) => boolean;
/**
* Whether validation is skipped, shared by every `schema-utils` in the process.
*/
Expand All @@ -95,14 +100,22 @@ export function enableValidation(): void;
* @returns {boolean} true when need validate, otherwise false
*/
export function needValidate(): boolean;
/**
* A check precompiled from a schema with `precompile`, it only says whether the value is valid.
* @callback PrecompiledCheck
* @param {EXPECTED_ANY} value value
* @returns {boolean} true when valid, otherwise false
*/
/**
* @param {Schema} schema schema
* @param {object[] | object} options options
* @param {ValidationErrorConfiguration=} configuration configuration
* @param {PrecompiledCheck=} check precompiled check of the schema, so a valid value never compiles the schema
* @returns {void}
*/
export function validate(
schema: Schema,
options: object[] | object,
configuration?: ValidationErrorConfiguration | undefined,
check?: PrecompiledCheck | undefined,
): void;
10 changes: 10 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,14 @@ export default defineConfig([
"n/prefer-node-protocol": "off",
},
},
{
files: ["bin/**/*.js"],
rules: {
// Node.js has always skipped the hashbang of the file it runs
"n/no-unsupported-features/es-syntax": [
"error",
{ ignores: ["hashbang"] },
],
},
},
]);
11 changes: 8 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
"author": "webpack Contrib (https://github.com/webpack-contrib)",
"main": "dist/index.js",
"types": "declarations/index.d.ts",
"bin": {
"schema-utils": "bin/schema-utils.js"
},
"files": [
"bin",
"dist",
"declarations"
],
Expand Down
Loading
Loading