diff --git a/package-lock.json b/package-lock.json index e0271c05..8c350c30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -929,7 +929,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -946,7 +945,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -958,7 +956,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -969,14 +966,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -993,7 +988,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1008,7 +1002,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -2667,7 +2660,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -3278,6 +3270,11 @@ "@types/sizzle": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "node_modules/@types/linkify-it": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", @@ -4856,7 +4853,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5506,8 +5502,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ejs": { "version": "3.1.9", @@ -6170,7 +6165,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -6186,7 +6180,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -7222,7 +7215,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -7392,7 +7384,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -8384,7 +8375,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -8993,7 +8983,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -10332,7 +10321,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -10347,7 +10335,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", - "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -10363,7 +10350,6 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, "engines": { "node": "14 || >=16.14" } @@ -11380,6 +11366,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11457,7 +11451,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -11469,7 +11462,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -11927,7 +11919,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -11953,7 +11944,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -12281,6 +12271,77 @@ "node": ">=8" } }, + "node_modules/ts-json-schema-generator": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.1.0.tgz", + "integrity": "sha512-w0Mlotd5SIUEGpXcg54UwpTRLXNtUaItt1XQO7mpW63TeV6gKmaGIInENxgccpUkj/ReBxbRoZAN79YU2Mc3YA==", + "dependencies": { + "@types/json-schema": "^7.0.15", + "commander": "^12.0.0", + "glob": "^10.3.12", + "json5": "^2.2.3", + "normalize-path": "^3.0.0", + "safe-stable-stringify": "^2.4.3", + "typescript": "^5.4.5" + }, + "bin": { + "ts-json-schema-generator": "bin/ts-json-schema-generator" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ts-json-schema-generator/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ts-json-schema-generator/node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-json-schema-generator/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-json-schema-generator/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -12547,7 +12608,6 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13354,7 +13414,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -13744,9 +13803,35 @@ "dependencies": { "@uwdata/mosaic-core": "^0.7.1", "@uwdata/mosaic-sql": "^0.7.0", - "@uwdata/vgplot": "^0.7.1" + "@uwdata/vgplot": "^0.7.1", + "ts-json-schema-generator": "^2.1.0" + }, + "devDependencies": { + "ajv": "^8.12.0" } }, + "packages/spec/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/spec/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "packages/sql": { "name": "@uwdata/mosaic-sql", "version": "0.7.0", @@ -13763,7 +13848,7 @@ "vega-embed": "^6.25.0" }, "devDependencies": { - "vite": "^5.0.10" + "vite": "^5.2.8" } }, "packages/vgplot": { diff --git a/packages/spec/package.json b/packages/spec/package.json index 01b05177..5860ba8e 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -23,15 +23,21 @@ }, "scripts": { "prebuild": "rimraf dist && mkdir dist", - "build": "tsc && node ../../esbuild.js mosaic-spec", + "build": "npm run types && node ../../esbuild.js mosaic-spec", "lint": "eslint src test", - "pretest": "tsc", + "types": "tsc -p tsconfig.json && npm run schema", + "schema": "ts-json-schema-generator -f tsconfig.json -p src/spec/Spec.ts -t Spec --no-type-check --no-ref-encode --functions hide > dist/mosaic-schema.json", + "pretest": "npm run prebuild && npm run types", "test": "mocha 'test/**/*-test.js' && tsc -p jsconfig.json", "prepublishOnly": "npm run test && npm run lint && npm run build" }, "dependencies": { "@uwdata/mosaic-core": "^0.7.1", "@uwdata/mosaic-sql": "^0.7.0", - "@uwdata/vgplot": "^0.7.1" + "@uwdata/vgplot": "^0.7.1", + "ts-json-schema-generator": "^2.1.0" + }, + "devDependencies": { + "ajv": "^8.12.0" } } diff --git a/packages/spec/src/spec/CSSStyles.ts b/packages/spec/src/spec/CSSStyles.ts new file mode 100644 index 00000000..bf95dd2a --- /dev/null +++ b/packages/spec/src/spec/CSSStyles.ts @@ -0,0 +1,9 @@ +type OmittedProperties = + | 'parentRule' + | 'getPropertyPriority' + | 'getPropertyValue' + | 'item' + | 'removeProperty' + | 'setProperty'; + +export type CSSStyles = Partial>; diff --git a/packages/spec/src/spec/Data.ts b/packages/spec/src/spec/Data.ts index 91e0761d..421550ac 100644 --- a/packages/spec/src/spec/Data.ts +++ b/packages/spec/src/spec/Data.ts @@ -37,7 +37,7 @@ export type DataArray = object[]; /** * A data definition that loads an external data file. */ -export interface DataFile { +export interface DataFile extends DataBaseOptions { /** * The data file to load. If no type option is provided, * the file suffix must be one of `.csv`, `.json`, or `.parquet`. diff --git a/packages/spec/src/spec/PlotAttribute.ts b/packages/spec/src/spec/PlotAttribute.ts index e24c1e14..2c8abca7 100644 --- a/packages/spec/src/spec/PlotAttribute.ts +++ b/packages/spec/src/spec/PlotAttribute.ts @@ -1,3 +1,4 @@ +import { CSSStyles } from './CSSStyles.js'; import { ParamRef } from './Param.js'; import { ColorScaleType, ColorScheme, ContinuousScaleType, DiscreteScaleType, @@ -124,7 +125,7 @@ export interface PlotAttributes { * [3]: https://www.w3.org/TR/css-values-4/#deprecated-quirky-length * [4]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentcolor_keyword */ - style?: string | Partial | null | ParamRef; + style?: string | CSSStyles | null | ParamRef; /** * How to distribute unused space in the **range** for *point* and *band* diff --git a/packages/spec/src/spec/marks/Marks.ts b/packages/spec/src/spec/marks/Marks.ts index a853e9c4..5b1d3ebd 100644 --- a/packages/spec/src/spec/marks/Marks.ts +++ b/packages/spec/src/spec/marks/Marks.ts @@ -4,10 +4,7 @@ import { PlotMarkData } from '../PlotFrom.js'; import { CurveName, FrameAnchor, Interval, Reducer, ScaleName } from '../PlotTypes.js'; import { Transform } from '../Transform.js'; -/** - * The set of known channel names. Channels in custom marks may use other names; - * these known names are enumerated for convenient autocomplete. - */ +/** The set of known channel names. */ export type ChannelName = | 'ariaLabel' | 'fill' @@ -38,8 +35,7 @@ export type ChannelName = | 'y' | 'y1' | 'y2' - | 'z' - | (string & Record); // custom channel; see also https://github.com/microsoft/TypeScript/issues/29729 + | 'z'; type ChannelScale = ScaleName | 'auto' | boolean | null; @@ -49,13 +45,12 @@ type ChannelScale = ScaleName | 'auto' | boolean | null; * - a field name, to extract the corresponding value for each datum * - an iterable of values, typically of the same length as the data * - a channel transform or SQL expression - * - a constant date, number, or boolean + * - a constant number or boolean * - null to represent no value */ export type ChannelValue = | any[] // column of values | (string & Record) // field or literal color; see also https://github.com/microsoft/TypeScript/issues/29729 - | Date // constant | number // constant | boolean // constant | null // constant @@ -912,9 +907,7 @@ export interface CurveAutoOptions { export type StackOffsetName = | 'center' | 'normalize' - | 'wiggle' - | ('expand' & Record) // deprecated; use normalize - | ('silhouette' & Record); // deprecated; use center + | 'wiggle'; /** * A stack offset method; one of: @@ -955,8 +948,6 @@ export type StackOrderName = 'value' | 'x' | 'y' | 'z' | 'sum' | 'appearance' | * * - a named stack order method such as *inside-out* or *sum* * - a field name, for natural order of the corresponding values - * - an accessor function, for natural order of the corresponding values - * - a comparator function for ordering data * - an array of explicit **z** values in the desired order */ export type StackOrder = diff --git a/packages/spec/test/load-specs.js b/packages/spec/test/load-specs.js index 8e9230c3..1bb951b1 100644 --- a/packages/spec/test/load-specs.js +++ b/packages/spec/test/load-specs.js @@ -29,3 +29,9 @@ export function loadJSON(name) { export function loadESM(name) { return readFile(join(ESM, `${name}.js`), 'utf8'); } + +export async function loadJSONSchema() { + const path = join(__dirname, '../dist/mosaic-schema.json'); + const text = await readFile(path, 'utf8'); + return parse(text); +} diff --git a/packages/spec/test/parse-test.js b/packages/spec/test/spec-test.js similarity index 53% rename from packages/spec/test/parse-test.js rename to packages/spec/test/spec-test.js index 58e72763..df0678ac 100644 --- a/packages/spec/test/parse-test.js +++ b/packages/spec/test/spec-test.js @@ -1,7 +1,25 @@ import assert from 'node:assert'; -import { specs, loadJSON, loadESM } from './load-specs.js'; +import ajv from 'ajv'; +import { specs, loadJSON, loadJSONSchema, loadESM } from './load-specs.js'; import { astToESM, parseSpec } from '../src/index.js'; +// initialize JSON schema validator +const validator = new ajv({ + allErrors: true, + allowUnionTypes: true, + verbose: true +}); +const schema = await loadJSONSchema(); +const validate = validator.compile(schema); + +// validate JSON schema +describe('JSON schema', () => { + it('is a valid JSON schema', () => { + assert.ok(validator.validateSchema(schema)); + }); +}); + +// validate specs, parsing, and generation for (const [name, spec] of specs) { describe(`Test specification: ${name}`, () => { it(`produces esm output`, async () => { @@ -23,5 +41,13 @@ for (const [name, spec] of specs) { `${name} did not round trip unchanged` ); }); + it(`passes JSON schema validation`, () => { + const valid = validate(spec); + if (!valid) { + console.error(validate.errors); + } + assert.ok(valid); + assert.ok(parseSpec(spec).toJSON()); + }); }); }