Skip to content

Commit e28cbd1

Browse files
committed
Refactor vitest integration to use a plugin
1 parent 3ffb389 commit e28cbd1

12 files changed

+139
-106
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,16 @@ script.
6161
`vitest-schema.config.js`
6262
```TypeScript
6363
import { defineConfig } from "vitest/config";
64-
import type { JsonSchemaCoverageProviderOptions } from "@hyperjump/json-schema-coverage/vitest/coverage-provider";
64+
import { jsonSchemaCoveragePlugin } from "@hyperjump/json-schema-coverage/vitest";
6565

6666
export default defineConfig({
67+
plugins: [jsonSchemaCoveragePlugin()],
6768
test: {
6869
globalSetup: ["./register-my-dialect.ts"], // Optional
6970
include: ["schema-tests/"], // Optional
7071
coverage: {
71-
provider: "custom",
72-
customProviderModule: "@hyperjump/json-schema-coverage/vitest/coverage-provider",
7372
include: ["schemas/**/*.json"] // Optional
74-
} as JsonSchemaCoverageProviderOptions
73+
}
7574
}
7675
});
7776
```
@@ -85,7 +84,6 @@ if vitest has coverage enabled, it will collect coverage data from those tests.
8584

8685
```JavaScript
8786
import { describe, expect, test } from "vitest";
88-
import "@hyperjump/json-schema-coverage/vitest-matchers";
8987

9088
describe("Worksheet", () => {
9189
test("matches with chai-style matcher", async () => {
@@ -107,7 +105,6 @@ for the validation to work.
107105

108106
```JavaScript
109107
import { describe, expect, test } from "vitest";
110-
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema-coverage/vitest";
111108

112109
describe("Worksheet", () => {
113110
beforeEach(async () => {
@@ -135,7 +132,6 @@ schemas from files in your code base.
135132

136133
```JavaScript
137134
import { describe, expect, test } from "vitest";
138-
import "@hyperjump/json-schema-coverage/vitest-matchers";
139135

140136
describe("Worksheet", () => {
141137
test("matches with schema", async () => {
@@ -158,6 +154,10 @@ These are the functions available when working with the vitest integration.
158154
import { ... } from "@hyperjump/json-schema-coverage/vitest"
159155
```
160156

157+
- **jsonSchemaCoveragePlugin**: () => VitestPlugin
158+
159+
A function that returns a Vitest plugin that registers matchers and sets up
160+
JSON Schema coverage if enabled.
161161
- **matchJsonSchema**: (uriOrSchema: string | SchemaObject | boolean) => Promise\<void>
162162

163163
A vitest matcher that can be used to validate a JSON-compatible value. It

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121
"type": "module",
2222
"exports": {
2323
".": "./src/index.js",
24-
"./vitest": "./src/vitest/index.js",
25-
"./vitest/coverage-provider": "./src/vitest/coverage-provider.js"
24+
"./vitest": "./src/vitest/index.js"
2625
},
2726
"scripts": {
2827
"lint": "eslint src",

src/vitest/build-coverage-maps.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
33
import { join, resolve } from "node:path";
44
import { glob } from "tinyglobby";
55
import ignore from "ignore";
6-
import { registerSchema } from "./json-schema-matchers.js";
6+
import { registerSchema } from "./index.js";
77
import { FileCoverageMapService } from "./file-coverage-map-service.js";
88

99
/**

src/vitest/coverage-provider.d.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/vitest/coverage-provider.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const JsonSchemaCoverageProviderModule = {
3030

3131
/** @implements CoverageProvider */
3232
class JsonSchemaCoverageProvider {
33-
name = "@hyperjump/json-schema-coverage/vitest/coverage-provider";
33+
name = "@hyperjump/json-schema-coverage/vitest";
3434

3535
ctx = /** @type Vitest */ ({});
3636

src/vitest/index.d.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
import type { SchemaObject } from "@hyperjump/json-schema";
21
import type { AsyncExpectationResult } from "@vitest/expect";
3-
import "vitest";
4-
5-
declare module "vitest" {
6-
interface Matchers<R = unknown> {
7-
matchJsonSchema: (uriOrSchema: string | SchemaObject | boolean) => Promise<R>;
8-
toMatchJsonSchema: (uriOrSchema: string | SchemaObject | boolean) => Promise<R>;
9-
}
10-
}
2+
import type { Plugin } from "vite";
3+
import type { SchemaObject } from "@hyperjump/json-schema";
4+
import JsonSchemaCoverageProvider from "./coverage-provider.js";
115

126
/**
137
* Register a schema in your code base by it's path.
@@ -25,6 +19,12 @@ export const registerSchema: (filePath: string) => Promise<void>;
2519
*/
2620
export const unregisterSchema: (filePath: string) => Promise<void>;
2721

22+
/**
23+
* Use this Vitest plugin in your Vitest config to enable the matchers and
24+
* coverage support.
25+
*/
26+
export const jsonSchemaCoveragePlugin: () => Plugin;
27+
2828
/**
2929
* A vitest matcher that can be used to validate a JSON-compatible value. It
3030
* can take a relative or full URI for a schema in your codebase. Use relative
@@ -33,8 +33,9 @@ export const unregisterSchema: (filePath: string) => Promise<void>;
3333
*
3434
* You can use this matcher with an inline schema as well, but you will only
3535
* get coverage for schemas that are in files.
36-
*
3736
*/
3837
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3938
export const matchJsonSchema: (instance: any, uriOrSchema: string | SchemaObject | boolean) => AsyncExpectationResult;
4039
export const toMatchJsonSchema: typeof matchJsonSchema;
40+
41+
export default JsonSchemaCoverageProvider;

src/vitest/index.js

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,73 @@
11
/** @module vitest */
2-
import { expect } from "vitest";
3-
import { matchJsonSchema, toMatchJsonSchema } from "./json-schema-matchers.js";
2+
import { readFile } from "node:fs/promises";
3+
import { extname } from "node:path";
4+
import { pathToFileURL } from "node:url";
5+
import { registerSchema as register, unregisterSchema as unregister } from "./json-schema.js";
6+
import { getKeywordId, getKeywordName } from "@hyperjump/json-schema/experimental";
7+
import YAML from "yaml";
8+
import { toAbsoluteIri } from "@hyperjump/uri";
9+
import JsonSchemaCoverageProvider from "./coverage-provider.js";
410

5-
expect.extend({
6-
matchJsonSchema: matchJsonSchema,
7-
toMatchJsonSchema: toMatchJsonSchema
8-
});
11+
/**
12+
* @import { SchemaObject } from "@hyperjump/json-schema"
13+
*/
14+
// @ts-expect-error There appears to be a bug in TypeScript
15+
/** @import * as API from "./index.d.ts" */
916

17+
export * from "./json-schema-coverage-plugin.js";
1018
export * from "./json-schema-matchers.js";
19+
20+
/** @type API.registerSchema */
21+
export const registerSchema = async (schemaPath) => {
22+
const text = await readFile(schemaPath, "utf-8");
23+
const extension = extname(schemaPath);
24+
25+
/** @type SchemaObject */
26+
let schema;
27+
switch (extension) {
28+
case ".json":
29+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
30+
schema = JSON.parse(text);
31+
break;
32+
case ".yaml":
33+
case ".yml":
34+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
35+
schema = YAML.parse(text);
36+
break;
37+
default:
38+
throw Error(`File of type '${extension}' is not supported.`);
39+
}
40+
41+
register(schema);
42+
};
43+
44+
/** @type (schemaPath: string) => Promise<void> */
45+
export const unregisterSchema = async (schemaPath) => {
46+
const text = await readFile(schemaPath, "utf-8");
47+
const extension = extname(schemaPath);
48+
49+
/** @type SchemaObject */
50+
let schema;
51+
switch (extension) {
52+
case ".json":
53+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
54+
schema = JSON.parse(text);
55+
break;
56+
case ".yaml":
57+
case ".yml":
58+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
59+
schema = YAML.parse(text);
60+
break;
61+
default:
62+
throw Error(`File of type '${extension}' is not supported.`);
63+
}
64+
65+
const dialectUri = toAbsoluteIri(/** @type string */ (schema.$schema));
66+
const idToken = getKeywordName(dialectUri, "https://json-schema.org/keyword/id")
67+
?? getKeywordId("https://json-schema.org/keyword/draft-04/id", dialectUri);
68+
const schemaUri = /** @type string */ (schema[idToken]) ?? pathToFileURL(schemaPath).toString();
69+
70+
unregister(schemaUri);
71+
};
72+
73+
export default JsonSchemaCoverageProvider;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @import { Plugin } from "vite"
3+
* @import { VitestPluginContext, ResolvedCoverageOptions } from "vitest/node"
4+
*/
5+
6+
import { resolve } from "path";
7+
8+
/** @type () => Plugin */
9+
export const jsonSchemaCoveragePlugin = () => {
10+
return {
11+
name: "json-schema-coverage-plugin",
12+
13+
/** @type (context: VitestPluginContext) => void */
14+
configureVitest(context) {
15+
const config = context.vitest.config;
16+
17+
const coverage = /** @type ResolvedCoverageOptions<"custom"> */ (config.coverage);
18+
coverage.provider = "custom";
19+
coverage.customProviderModule = resolve(import.meta.dirname, "./coverage-provider.js");
20+
21+
config.setupFiles.push(resolve(import.meta.dirname, "./register-matchers.js"));
22+
}
23+
};
24+
};

src/vitest/json-schema-matchers.js

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import { randomUUID } from "node:crypto";
22
import { existsSync } from "node:fs";
3-
import { readFile, writeFile } from "node:fs/promises";
4-
import { extname, join } from "node:path";
5-
import { pathToFileURL } from "node:url";
6-
import { registerSchema as register, unregisterSchema as unregister, validate } from "./json-schema.js";
7-
import { getKeywordId, getKeywordName, BASIC } from "@hyperjump/json-schema/experimental";
8-
import YAML from "yaml";
3+
import { writeFile } from "node:fs/promises";
4+
import { join } from "node:path";
5+
import { registerSchema, unregisterSchema, validate } from "./json-schema.js";
6+
import { BASIC } from "@hyperjump/json-schema/experimental";
97
import { FileCoverageMapService } from "./file-coverage-map-service.js";
108
import { TestCoverageEvaluationPlugin } from "../test-coverage-evaluation-plugin.js";
11-
import { toAbsoluteIri } from "@hyperjump/uri";
129

1310
/**
14-
* @import { OutputUnit, SchemaObject } from "@hyperjump/json-schema"
11+
* @import { OutputUnit } from "@hyperjump/json-schema"
1512
* @import * as API from "./index.d.ts"
1613
*/
1714

@@ -51,12 +48,12 @@ export const matchJsonSchema = async (instance, uriOrSchema) => {
5148
} else {
5249
const schema = uriOrSchema;
5350
const uri = `urn:uuid:${randomUUID()}`;
54-
register(schema, uri, "https://json-schema.org/draft/2020-12/schema");
51+
registerSchema(schema, uri, "https://json-schema.org/draft/2020-12/schema");
5552
try {
5653
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
5754
output = await validate(uri, instance, BASIC);
5855
} finally {
59-
unregister(uri);
56+
unregisterSchema(uri);
6057
}
6158
}
6259

@@ -67,56 +64,3 @@ export const matchJsonSchema = async (instance, uriOrSchema) => {
6764
};
6865

6966
export const toMatchJsonSchema = matchJsonSchema;
70-
71-
/** @type API.registerSchema */
72-
export const registerSchema = async (schemaPath) => {
73-
const text = await readFile(schemaPath, "utf-8");
74-
const extension = extname(schemaPath);
75-
76-
/** @type SchemaObject */
77-
let schema;
78-
switch (extension) {
79-
case ".json":
80-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
81-
schema = JSON.parse(text);
82-
break;
83-
case ".yaml":
84-
case ".yml":
85-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
86-
schema = YAML.parse(text);
87-
break;
88-
default:
89-
throw Error(`File of type '${extension}' is not supported.`);
90-
}
91-
92-
register(schema);
93-
};
94-
95-
/** @type (schemaPath: string) => Promise<void> */
96-
export const unregisterSchema = async (schemaPath) => {
97-
const text = await readFile(schemaPath, "utf-8");
98-
const extension = extname(schemaPath);
99-
100-
/** @type SchemaObject */
101-
let schema;
102-
switch (extension) {
103-
case ".json":
104-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
105-
schema = JSON.parse(text);
106-
break;
107-
case ".yaml":
108-
case ".yml":
109-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
110-
schema = YAML.parse(text);
111-
break;
112-
default:
113-
throw Error(`File of type '${extension}' is not supported.`);
114-
}
115-
116-
const dialectUri = toAbsoluteIri(/** @type string */ (schema.$schema));
117-
const idToken = getKeywordName(dialectUri, "https://json-schema.org/keyword/id")
118-
?? getKeywordId("https://json-schema.org/keyword/draft-04/id", dialectUri);
119-
const schemaUri = /** @type string */ (schema[idToken]) ?? pathToFileURL(schemaPath).toString();
120-
121-
unregister(schemaUri);
122-
};

src/vitest/register-matchers.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { SchemaObject } from "@hyperjump/json-schema";
2+
import "vitest";
3+
4+
declare module "vitest" {
5+
interface Matchers<R = unknown> {
6+
matchJsonSchema: (uriOrSchema: string | SchemaObject | boolean) => Promise<R>;
7+
toMatchJsonSchema: (uriOrSchema: string | SchemaObject | boolean) => Promise<R>;
8+
}
9+
}

0 commit comments

Comments
 (0)