diff --git a/.gitignore b/.gitignore index d0e295bcda..9fbbcad0ac 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ package-lock.json .DS_Store tsconfig.*.tsbuildinfo src-generated +packages/core/schema __filterSpecs.js \ No newline at end of file diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 180a66d871..d7542d65b2 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -2140,15 +2140,23 @@ "dev": true }, "ajv": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", - "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "dependencies": { + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + } } }, "ajv-errors": { @@ -2401,6 +2409,35 @@ "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", "dev": true }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "dev": true, + "requires": { + "follow-redirects": "1.5.10" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + } + } + } + }, "babel-jest": { "version": "26.0.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.0.1.tgz", diff --git a/e2e/package.json b/e2e/package.json index 976ec86d66..203fe5052f 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -10,6 +10,8 @@ "@babel/preset-env": "~7.8.3", "@types/node": "^10.12.18", "@types/semver": "~6.2.0", + "ajv": "^6.12.2", + "axios": "^0.19.2", "chai": "~4.2.0", "chai-as-promised": "~7.1.1", "cross-env": "~5.2.0", diff --git a/e2e/tasks/run-e2e-tests.ts b/e2e/tasks/run-e2e-tests.ts index e39b128143..5a8043d8fb 100644 --- a/e2e/tasks/run-e2e-tests.ts +++ b/e2e/tasks/run-e2e-tests.ts @@ -1,6 +1,6 @@ import fs = require('fs'); import * as path from 'path'; -import * as execa from 'execa'; +import execa from 'execa'; import * as semver from 'semver'; import * as os from 'os'; import { from, defer } from 'rxjs'; diff --git a/e2e/test/mono-schema/package.json b/e2e/test/mono-schema/package.json new file mode 100644 index 0000000000..16c593215b --- /dev/null +++ b/e2e/test/mono-schema/package.json @@ -0,0 +1,7 @@ +{ + "name": "mono-schema", + "description": "A test for the 'mono-schema'. A json schema that got merged from all the objects and put into @stryker-mutator/core/schema", + "scripts": { + "test": "mocha --require \"ts-node/register\" verify/verify.ts" + } +} diff --git a/e2e/test/mono-schema/test/invalid.json b/e2e/test/mono-schema/test/invalid.json new file mode 100644 index 0000000000..317deee6fb --- /dev/null +++ b/e2e/test/mono-schema/test/invalid.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "allowConsoleColors": true, + "babel": { + "options": { + "compact": "invalid" + } + }, + "jest": { + "config": "" + }, + "wct": { + "configFile": {} + }, + "jasmineConfigFile": false, + "webpack": { + "configFile": [] + }, + "karma": { + "ngConfig": { + "testArguments": "--test test" + } + }, + "tsconfig": { + "moduleResolution": "invalid" + } +} diff --git a/e2e/test/mono-schema/test/valid.json b/e2e/test/mono-schema/test/valid.json new file mode 100644 index 0000000000..6f06fba585 --- /dev/null +++ b/e2e/test/mono-schema/test/valid.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "allowConsoleColors": true, + "babel": { + "options": { + "compact": "auto" + } + }, + "jest": { + "config": {} + }, + "wct": { + "configFile": "a file" + }, + "jasmineConfigFile": "A file", + "webpack": { + "configFile": "webpack.config.js" + }, + "karma": { + "ngConfig": { + "testArguments": { "test": "test" } + } + }, + "tsconfig": { + "moduleResolution": "node" + } +} diff --git a/e2e/test/mono-schema/verify/verify.ts b/e2e/test/mono-schema/verify/verify.ts new file mode 100644 index 0000000000..ae04d70dcb --- /dev/null +++ b/e2e/test/mono-schema/verify/verify.ts @@ -0,0 +1,119 @@ +import { expect } from 'chai'; +import valid from '../test/valid.json'; +import invalid from '../test/invalid.json'; +import monoSchema from '@stryker-mutator/core/schema/stryker-schema.json'; +import Ajv from 'ajv'; +import Axios from 'axios'; +import { beforeEach } from 'mocha'; + +const ajv = new Ajv({ + async: true, + allErrors: true, + loadSchema: async (url) => { + const content = await Axios.get(url); + delete content.data.$schema; + return content.data; + }, +}); + +describe('The Stryker meta schema', () => { + let validator: Ajv.ValidateFunction; + + beforeEach(async () => { + validator = await ajv.compileAsync(monoSchema); + }); + + it('should validate a valid schema', async () => { + expect(validator(valid), ajv.errorsText(validator.errors)).true; + }); + + it('should invalidate a invalid schema', async () => { + expect(validator(invalid)).false; + expect(validator.errors).deep.eq(expectedErrors); + }); + + const expectedErrors = [ + { + keyword: 'enum', + dataPath: '.babel.options.compact', + schemaPath: 'http://json.schemastore.org/babelrc/properties/compact/enum', + params: { + allowedValues: ['auto', true, false], + }, + message: 'should be equal to one of the allowed values', + }, + { + keyword: 'type', + dataPath: '.jasmineConfigFile', + schemaPath: '#/properties/jasmineConfigFile/type', + params: { + type: 'string', + }, + message: 'should be string', + }, + { + keyword: 'type', + dataPath: '.jest.config', + schemaPath: '#/properties/jest/properties/config/type', + params: { + type: 'object', + }, + message: 'should be object', + }, + { + keyword: 'type', + dataPath: '.karma.ngConfig.testArguments', + schemaPath: '#/definitions/karmaNgConfigOptions/properties/testArguments/type', + params: { + type: 'object', + }, + message: 'should be object', + }, + { + keyword: 'enum', + dataPath: '.tsconfig.moduleResolution', + schemaPath: + 'http://json.schemastore.org/tsconfig#/definitions/compilerOptionsDefinition/properties/compilerOptions/properties/moduleResolution/anyOf/0/enum', + params: { + allowedValues: ['Classic', 'Node'], + }, + message: 'should be equal to one of the allowed values', + }, + { + keyword: 'pattern', + dataPath: '.tsconfig.moduleResolution', + schemaPath: + 'http://json.schemastore.org/tsconfig#/definitions/compilerOptionsDefinition/properties/compilerOptions/properties/moduleResolution/anyOf/1/pattern', + params: { + pattern: '^(([Nn]ode)|([Cc]lassic))$', + }, + message: 'should match pattern "^(([Nn]ode)|([Cc]lassic))$"', + }, + { + keyword: 'anyOf', + dataPath: '.tsconfig.moduleResolution', + schemaPath: + 'http://json.schemastore.org/tsconfig#/definitions/compilerOptionsDefinition/properties/compilerOptions/properties/moduleResolution/anyOf', + params: {}, + message: 'should match some schema in anyOf', + }, + { + keyword: 'type', + dataPath: '.wct.configFile', + schemaPath: '#/properties/wct/properties/configFile/type', + params: { + type: 'string', + }, + message: 'should be string', + }, + { + keyword: 'type', + dataPath: '.webpack.configFile', + schemaPath: '#/properties/webpack/properties/configFile/type', + params: { + type: 'string', + }, + message: 'should be string', + }, + ]; +}); diff --git a/e2e/test/plugin-options-validation/package.json b/e2e/test/plugin-options-validation/package.json index fe203e7e5b..07ff9e9ca6 100644 --- a/e2e/test/plugin-options-validation/package.json +++ b/e2e/test/plugin-options-validation/package.json @@ -1,4 +1,5 @@ { + "name": "plugin-options-validation", "scripts": { "pretest": "rimraf stryker.log", "test": "stryker run --fileLogLevel info stryker-error-in-plugin-options.conf.json || exit 0", diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index c7c85e3b6a..0e691f3f81 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -1,9 +1,9 @@ { "extends": "../tsconfig.settings.json", "compilerOptions": { - "composite": false, "declaration": false, "importHelpers": false, + "esModuleInterop": true, "rootDir": ".", "types": [ "mocha", diff --git a/package.json b/package.json index 5d59e2fec5..4caea9fa33 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "lint:log": "eslint . --ext .ts,.tsx -f compact -o lint.log", "lint:fix": "eslint . --ext .ts,.tsx --fix", "clean": "rimraf \"packages/api/!(stryker.conf)+(.d.ts|.js|.map)\" \"packages/*/+(test|src)/**/*+(.d.ts|.js|.map)\" \"packages/*/{.nyc_output,reports,coverage,src-generated,*.tsbuildinfo,.stryker-tmp}\"", - "generate": "node tasks/generate-json-schema-to-ts.js", + "clean": "rimraf \"packages/api/!(stryker.conf)+(.d.ts|.js|.map)\" \"packages/*/+(test|src)/**/*+(.d.ts|.js|.map)\" \"packages/*/{.nyc_output,reports,coverage,src-generated,*.tsbuildinfo}\"", + "generate": "node tasks/generate-json-schema-to-ts.js && node tasks/generate-mono-schema.js", "prebuild": "npm run generate", "build": "tsc -b && lerna run build", "test": "npm run mocha", diff --git a/packages/core/.npmignore b/packages/core/.npmignore index d5f6bc8cab..8733fb52ef 100644 --- a/packages/core/.npmignore +++ b/packages/core/.npmignore @@ -4,6 +4,7 @@ !src/** src/**/*.map src/**/*.ts +!schema/*.json !src/**/*.d.ts !readme.md !LICENSE diff --git a/packages/core/src/initializer/StrykerConfigWriter.ts b/packages/core/src/initializer/StrykerConfigWriter.ts index d02b887fc7..3de7c3cffc 100644 --- a/packages/core/src/initializer/StrykerConfigWriter.ts +++ b/packages/core/src/initializer/StrykerConfigWriter.ts @@ -108,7 +108,7 @@ export default class StrykerConfigWriter { private async writeJsonConfig(commentedConfig: Partial) { this.out(`Writing & formatting ${STRYKER_JSON_CONFIG_FILE}...`); const typedConfig = { - $schema: 'https://raw.githubusercontent.com/stryker-mutator/stryker/master/packages/api/schema/stryker-core.json', + $schema: 'https://unpkg.com/@stryker-mutator/core/schema/stryker-schema.json', ...commentedConfig, }; const formattedConfig = this.stringify(typedConfig); diff --git a/packages/typescript/schema/typescript-options.json b/packages/typescript/schema/typescript-options.json index a4c1de2230..9a519204a4 100644 --- a/packages/typescript/schema/typescript-options.json +++ b/packages/typescript/schema/typescript-options.json @@ -11,7 +11,7 @@ "tsconfig": { "description": "Override tsconfig from your tsconfig.json file", "type": "object", - "$ref": "http://json.schemastore.org/tsconfig" + "$ref": "http://json.schemastore.org/tsconfig#/definitions/compilerOptionsDefinition/properties/compilerOptions" } } } diff --git a/tasks/generate-json-schema-to-ts.js b/tasks/generate-json-schema-to-ts.js index 338cb4dbc9..bf57a52195 100644 --- a/tasks/generate-json-schema-to-ts.js +++ b/tasks/generate-json-schema-to-ts.js @@ -9,15 +9,6 @@ const mkdir = promisify(fs.mkdir); const resolveFromParent = path.resolve.bind(path, __dirname, '..'); const globAsPromised = promisify(glob); -const noNetworkHttpResolver = { - order: 1, - canRead: /^http:/i, - - read(_file, callback) { - callback(undefined, {}); - } -} - /** * * @param {string} schemaFile @@ -34,9 +25,7 @@ async function generate(schemaFile) { }, $refOptions: { resolve: { - http: false, - // @ts-ignore - noNetworkHttpResolver + http: false // We're not interesting in generating exact babel / tsconfig / etc types } }, bannerComment: `/** @@ -50,7 +39,7 @@ async function generate(schemaFile) { } async function generateAllSchemas() { - const files = await globAsPromised('packages/*/schema/*.json', { cwd: resolveFromParent() }); + const files = await globAsPromised('packages/!(core)/schema/*.json', { cwd: resolveFromParent() }); await Promise.all(files.map(fileName => generate(resolveFromParent(fileName)))); } generateAllSchemas().catch(err => { @@ -59,39 +48,41 @@ generateAllSchemas().catch(err => { }); function preprocessSchema(inputSchema) { + const cleanedSchema = cleanExternalRef(inputSchema); + try { - switch (inputSchema.type) { + switch (cleanedSchema.type) { case 'object': - const inputRequired = inputSchema.required || []; + const inputRequired = cleanedSchema.required || []; const outputSchema = { - ...inputSchema, - properties: preprocessProperties(inputSchema.properties), - definitions: preprocessProperties(inputSchema.definitions), - required: preprocessRequired(inputSchema.properties, inputRequired) + ...cleanedSchema, + properties: preprocessProperties(cleanedSchema.properties), + definitions: preprocessProperties(cleanedSchema.definitions), + required: preprocessRequired(cleanedSchema.properties, inputRequired) } - if (inputSchema.definitions) { - outputSchema.definitions = preprocessProperties(inputSchema.definitions); + if (cleanedSchema.definitions) { + outputSchema.definitions = preprocessProperties(cleanedSchema.definitions); } return outputSchema; case 'array': return { - ...inputSchema, - items: preprocessSchema(inputSchema.items) + ...cleanedSchema, + items: preprocessSchema(cleanedSchema.items) } default: - if (inputSchema.$ref) { + if (cleanedSchema.$ref) { // Workaround for: https://github.com/bcherny/json-schema-to-typescript/issues/193 return { - $ref: inputSchema.$ref + $ref: cleanedSchema.$ref } } - if(inputSchema.oneOf) { + if(cleanedSchema.oneOf) { return { - ...inputSchema, - oneOf: inputSchema.oneOf.map(preprocessSchema) + ...cleanedSchema, + oneOf: cleanedSchema.oneOf.map(preprocessSchema) } } - return inputSchema; + return cleanedSchema; } } catch (err) { if (err instanceof SchemaError) { @@ -112,6 +103,16 @@ function preprocessProperties(inputProperties) { } } +function cleanExternalRef(inputSchema) { + if(inputSchema.$ref && inputSchema.$ref.startsWith('http')) { + return { + ...inputSchema, + $ref: undefined + } + } + return inputSchema; +} + function preprocessRequired(inputProperties, inputRequired) { if (inputProperties) { return Object.entries(inputProperties) diff --git a/tasks/generate-mono-schema.js b/tasks/generate-mono-schema.js new file mode 100644 index 0000000000..8ccf88a048 --- /dev/null +++ b/tasks/generate-mono-schema.js @@ -0,0 +1,32 @@ +// @ts-check +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const { promisify } = require('util'); +const writeFile = promisify(fs.writeFile); +const readFile = promisify(fs.readFile); +const mkdir = promisify(fs.mkdir); +const resolveFromParent = path.resolve.bind(path, __dirname, '..'); +const globAsPromised = promisify(glob); + +/** + * Build the mono schema based on all schemas from the plugin as well as the Stryker core schema + */ +async function buildMonoSchema() { + const schemaFiles = await globAsPromised('packages/!(core)/schema/*.json', { cwd: resolveFromParent() }); + const allContent = await Promise.all(schemaFiles.map(schemaFile => readFile(resolveFromParent(schemaFile), 'utf8'))) + const allSchemas = allContent.map(content => JSON.parse(content)); + const monoSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'StrykerMonoSchema', + description: 'Options for Stryker for JS and TypeScript and all officially supported plugins.', + type: 'object', + properties: allSchemas.reduce((props, schema) => ({ ...props, ...schema.properties }), {}), + definitions: allSchemas.reduce((props, schema) => ({ ...props, ...schema.definitions }), {}) + } + const outFile = resolveFromParent('packages', 'core', 'schema', 'stryker-schema.json'); + await mkdir(path.dirname(outFile), { recursive: true }); + await writeFile(outFile, JSON.stringify(monoSchema, null, 2)); + console.info(`✅ Merged ${schemaFiles.length} schemas into ${path.relative(resolveFromParent(), outFile)}`); +} +buildMonoSchema().catch(console.error);