Modify ajv function validation (#2899)

* Remove ajv-keywords

* Linting
This commit is contained in:
Jeffrey Posnick
2021-07-28 11:02:41 -07:00
committed by GitHub
parent 6092b9bd51
commit d4020f3516
10 changed files with 120 additions and 135 deletions
+9 -17
View File
@@ -46,31 +46,23 @@ async function generateWorkboxBuildJSONSchema(packagePath) {
]; ];
for (const optionType of optionTypes) { for (const optionType of optionTypes) {
const schema = generator.getSchemaForSymbol(optionType); const schema = generator.getSchemaForSymbol(optionType);
// Ideally, we'd set typeOfKeyword so that functions could be represented.
// Instead, we need to hardcode a few overrides to deal with functions.
// See https://github.com/YousefED/typescript-json-schema/issues/424
if (schema.properties.manifestTransforms) { if (schema.properties.manifestTransforms) {
schema.properties.manifestTransforms.items = {typeof: 'function'}; schema.properties.manifestTransforms.items = {};
} }
if (schema.properties.exclude) { if (schema.properties.exclude) {
schema.properties.exclude.items.anyOf = [ schema.properties.exclude.items = {};
{'$ref': '#/definitions/RegExp'},
{type: 'string'},
{typeof: 'function'},
];
} }
if (schema.properties.include) { if (schema.properties.include) {
schema.properties.include.items.anyOf = [ schema.properties.include.items = {};
{'$ref': '#/definitions/RegExp'},
{type: 'string'},
{typeof: 'function'},
];
} }
if (schema.definitions.RouteMatchCallback) { if (schema.definitions.RouteMatchCallback) {
delete schema.definitions.RouteMatchCallback.type; schema.definitions.RouteMatchCallback = {};
delete schema.definitions.RouteMatchCallback.additionalProperties;
schema.definitions.RouteMatchCallback.typeof = 'function';
} }
await fse.writeJSON(upath.join(packagePath, 'src', 'schema', await fse.writeJSON(upath.join(packagePath, 'src', 'schema',
`${optionType}.json`), schema, {spaces: 2}); `${optionType}.json`), schema, {spaces: 2});
} }
-1
View File
@@ -29,7 +29,6 @@
"@rollup/plugin-replace": "^2.4.1", "@rollup/plugin-replace": "^2.4.1",
"@surma/rollup-plugin-off-main-thread": "^1.4.1", "@surma/rollup-plugin-off-main-thread": "^1.4.1",
"ajv": "^8.6.0", "ajv": "^8.6.0",
"ajv-keywords": "^5.0.0",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"fast-json-stable-stringify": "^2.1.0", "fast-json-stable-stringify": "^2.1.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
+2
View File
@@ -118,4 +118,6 @@ export const errors = {
configure a runtimeCaching route that will use the preloaded response.`, configure a runtimeCaching route that will use the preloaded response.`,
'cache-name-required': ol`When using cache expiration, you must also 'cache-name-required': ol`When using cache expiration, you must also
configure a custom cacheName.`, configure a custom cacheName.`,
'manifest-transforms': ol`When using manifestTransforms, you must provide
an array of functions.`,
}; };
@@ -9,7 +9,6 @@
import {betterAjvErrors} from '@apideck/better-ajv-errors'; import {betterAjvErrors} from '@apideck/better-ajv-errors';
import {oneLine as ol} from 'common-tags'; import {oneLine as ol} from 'common-tags';
import Ajv, {JSONSchemaType} from 'ajv'; import Ajv, {JSONSchemaType} from 'ajv';
import ajvKeywords from 'ajv-keywords';
import {errors} from './errors'; import {errors} from './errors';
@@ -21,15 +20,18 @@ import {
WebpackInjectManifestOptions, WebpackInjectManifestOptions,
} from '../types'; } from '../types';
type MethodNames = 'GenerateSW' | 'GetManifest' | 'InjectManifest' | type MethodNames =
'WebpackGenerateSW' | 'WebpackInjectManifest'; | 'GenerateSW'
| 'GetManifest'
| 'InjectManifest'
| 'WebpackGenerateSW'
| 'WebpackInjectManifest';
const ajv = new Ajv({ const ajv = new Ajv({
useDefaults: true, useDefaults: true,
}); });
ajvKeywords(ajv, ['typeof']);
const DEFAULT_EXCLUDE_VALUE= [/\.map$/, /^manifest.*\.js$/]; const DEFAULT_EXCLUDE_VALUE = [/\.map$/, /^manifest.*\.js$/];
export class WorkboxConfigError extends Error { export class WorkboxConfigError extends Error {
constructor(message?: string) { constructor(message?: string) {
@@ -41,10 +43,12 @@ export class WorkboxConfigError extends Error {
function validate<T>(input: unknown, methodName: MethodNames): T { function validate<T>(input: unknown, methodName: MethodNames): T {
// Don't mutate input: https://github.com/GoogleChrome/workbox/issues/2158 // Don't mutate input: https://github.com/GoogleChrome/workbox/issues/2158
const inputCopy = Object.assign({}, input); const inputCopy = Object.assign({}, input);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const jsonSchema: JSONSchemaType<T> = require(`../schema/${methodName}Options.json`); const jsonSchema: JSONSchemaType<T> = require(`../schema/${methodName}Options.json`);
const validate = ajv.compile(jsonSchema); const validate = ajv.compile(jsonSchema);
if (validate(inputCopy)) { if (validate(inputCopy)) {
// All methods support manifestTransforms, so validate it here.
ensureValidManifestTransforms(inputCopy);
return inputCopy; return inputCopy;
} }
@@ -56,19 +60,42 @@ function validate<T>(input: unknown, methodName: MethodNames): T {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
schema: jsonSchema as any, schema: jsonSchema as any,
}); });
const messages = betterErrors.map((err) => ol`[${err.path}] ${err.message}. const messages = betterErrors.map(
${err.suggestion ? err.suggestion : ''}`); (err) => ol`[${err.path}] ${err.message}.
${err.suggestion ? err.suggestion : ''}`,
);
throw new WorkboxConfigError(messages.join('\n\n')); throw new WorkboxConfigError(messages.join('\n\n'));
} }
function ensureValidManifestTransforms(
options:
| GenerateSWOptions
| GetManifestOptions
| InjectManifestOptions
| WebpackGenerateSWOptions
| WebpackInjectManifestOptions,
): void {
if (
'manifestTransforms' in options &&
!(
Array.isArray(options.manifestTransforms) &&
options.manifestTransforms.every((item) => typeof item === 'function')
)
) {
throw new WorkboxConfigError(errors['manifest-transforms']);
}
}
function ensureValidNavigationPreloadConfig( function ensureValidNavigationPreloadConfig(
options: GenerateSWOptions | WebpackGenerateSWOptions, options: GenerateSWOptions | WebpackGenerateSWOptions,
): void { ): void {
if (options.navigationPreload && if (
(!Array.isArray(options.runtimeCaching) || options.navigationPreload &&
options.runtimeCaching.length === 0)) { (!Array.isArray(options.runtimeCaching) ||
throw new WorkboxConfigError(errors['nav-preload-runtime-caching']); options.runtimeCaching.length === 0)
) {
throw new WorkboxConfigError(errors['nav-preload-runtime-caching']);
} }
} }
@@ -76,7 +103,10 @@ function ensureValidCacheExpiration(
options: GenerateSWOptions | WebpackGenerateSWOptions, options: GenerateSWOptions | WebpackGenerateSWOptions,
): void { ): void {
for (const runtimeCaching of options.runtimeCaching || []) { for (const runtimeCaching of options.runtimeCaching || []) {
if (runtimeCaching.options?.expiration && !runtimeCaching.options?.cacheName) { if (
runtimeCaching.options?.expiration &&
!runtimeCaching.options?.cacheName
) {
throw new WorkboxConfigError(errors['cache-name-required']); throw new WorkboxConfigError(errors['cache-name-required']);
} }
} }
@@ -85,10 +115,14 @@ function ensureValidCacheExpiration(
function ensureValidRuntimeCachingOrGlobDirectory( function ensureValidRuntimeCachingOrGlobDirectory(
options: GenerateSWOptions, options: GenerateSWOptions,
): void { ): void {
if (!options.globDirectory && if (
(!Array.isArray(options.runtimeCaching) || !options.globDirectory &&
options.runtimeCaching.length === 0)) { (!Array.isArray(options.runtimeCaching) ||
throw new WorkboxConfigError(errors['no-manifest-entries-or-runtime-caching']); options.runtimeCaching.length === 0)
) {
throw new WorkboxConfigError(
errors['no-manifest-entries-or-runtime-caching'],
);
} }
} }
@@ -97,6 +131,7 @@ export function validateGenerateSWOptions(input: unknown): GenerateSWOptions {
ensureValidNavigationPreloadConfig(validatedOptions); ensureValidNavigationPreloadConfig(validatedOptions);
ensureValidCacheExpiration(validatedOptions); ensureValidCacheExpiration(validatedOptions);
ensureValidRuntimeCachingOrGlobDirectory(validatedOptions); ensureValidRuntimeCachingOrGlobDirectory(validatedOptions);
return validatedOptions; return validatedOptions;
} }
@@ -104,27 +139,47 @@ export function validateGetManifestOptions(input: unknown): GetManifestOptions {
return validate<GetManifestOptions>(input, 'GetManifest'); return validate<GetManifestOptions>(input, 'GetManifest');
} }
export function validateInjectManifestOptions(input: unknown): InjectManifestOptions { export function validateInjectManifestOptions(
input: unknown,
): InjectManifestOptions {
return validate<InjectManifestOptions>(input, 'InjectManifest'); return validate<InjectManifestOptions>(input, 'InjectManifest');
} }
// The default `exclude: [/\.map$/, /^manifest.*\.js$/]` value can't be // The default `exclude: [/\.map$/, /^manifest.*\.js$/]` value can't be
// represented in the JSON schema, so manually set it for the webpack options. // represented in the JSON schema, so manually set it for the webpack options.
export function validateWebpackGenerateSWOptions(input: unknown): WebpackGenerateSWOptions { export function validateWebpackGenerateSWOptions(
const inputWithExcludeDefault = Object.assign({ input: unknown,
// Make a copy, as exclude can be mutated when used. ): WebpackGenerateSWOptions {
exclude: Array.from(DEFAULT_EXCLUDE_VALUE), const inputWithExcludeDefault = Object.assign(
}, input); {
const validatedOptions = validate<WebpackGenerateSWOptions>(inputWithExcludeDefault, 'WebpackGenerateSW'); // Make a copy, as exclude can be mutated when used.
exclude: Array.from(DEFAULT_EXCLUDE_VALUE),
},
input,
);
const validatedOptions = validate<WebpackGenerateSWOptions>(
inputWithExcludeDefault,
'WebpackGenerateSW',
);
ensureValidNavigationPreloadConfig(validatedOptions); ensureValidNavigationPreloadConfig(validatedOptions);
ensureValidCacheExpiration(validatedOptions); ensureValidCacheExpiration(validatedOptions);
return validatedOptions; return validatedOptions;
} }
export function validateWebpackInjectManifestOptions(input: unknown): WebpackInjectManifestOptions { export function validateWebpackInjectManifestOptions(
const inputWithExcludeDefault = Object.assign({ input: unknown,
// Make a copy, as exclude can be mutated when used. ): WebpackInjectManifestOptions {
exclude: Array.from(DEFAULT_EXCLUDE_VALUE), const inputWithExcludeDefault = Object.assign(
}, input); {
return validate<WebpackInjectManifestOptions>(inputWithExcludeDefault, 'WebpackInjectManifest'); // Make a copy, as exclude can be mutated when used.
exclude: Array.from(DEFAULT_EXCLUDE_VALUE),
},
input,
);
return validate<WebpackInjectManifestOptions>(
inputWithExcludeDefault,
'WebpackInjectManifest',
);
} }
@@ -20,9 +20,7 @@
}, },
"manifestTransforms": { "manifestTransforms": {
"type": "array", "type": "array",
"items": { "items": {}
"typeof": "function"
}
}, },
"maximumFileSizeToCacheInBytes": { "maximumFileSizeToCacheInBytes": {
"default": 2097152, "default": 2097152,
@@ -861,10 +859,7 @@
"onabort" "onabort"
] ]
}, },
"RouteMatchCallback": { "RouteMatchCallback": {}
"description": "The \"match\" callback is used to determine if a `Route` should apply for a\nparticular URL and request. When matching occurs in response to a fetch\nevent from the client, the `event` object is also supplied. However, since\nthe match callback can be invoked outside of a fetch event, matching logic\nshould not assume the `event` object will always be available.\nIf the match callback returns a truthy value, the matching route's\n`RouteHandlerCallback` will be invoked immediately. If the value returned\nis a non-empty array or object, that value will be set on the handler's\n`options.params` argument.",
"typeof": "function"
}
}, },
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
} }
@@ -20,9 +20,7 @@
}, },
"manifestTransforms": { "manifestTransforms": {
"type": "array", "type": "array",
"items": { "items": {}
"typeof": "function"
}
}, },
"maximumFileSizeToCacheInBytes": { "maximumFileSizeToCacheInBytes": {
"default": 2097152, "default": 2097152,
@@ -753,10 +751,7 @@
"onabort" "onabort"
] ]
}, },
"RouteMatchCallback": { "RouteMatchCallback": {}
"description": "The \"match\" callback is used to determine if a `Route` should apply for a\nparticular URL and request. When matching occurs in response to a fetch\nevent from the client, the `event` object is also supplied. However, since\nthe match callback can be invoked outside of a fetch event, matching logic\nshould not assume the `event` object will always be available.\nIf the match callback returns a truthy value, the matching route's\n`RouteHandlerCallback` will be invoked immediately. If the value returned\nis a non-empty array or object, that value will be set on the handler's\n`options.params` argument.",
"typeof": "function"
}
}, },
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
} }
@@ -20,9 +20,7 @@
}, },
"manifestTransforms": { "manifestTransforms": {
"type": "array", "type": "array",
"items": { "items": {}
"typeof": "function"
}
}, },
"maximumFileSizeToCacheInBytes": { "maximumFileSizeToCacheInBytes": {
"default": 2097152, "default": 2097152,
@@ -765,10 +763,7 @@
"onabort" "onabort"
] ]
}, },
"RouteMatchCallback": { "RouteMatchCallback": {}
"description": "The \"match\" callback is used to determine if a `Route` should apply for a\nparticular URL and request. When matching occurs in response to a fetch\nevent from the client, the `event` object is also supplied. However, since\nthe match callback can be invoked outside of a fetch event, matching logic\nshould not assume the `event` object will always be available.\nIf the match callback returns a truthy value, the matching route's\n`RouteHandlerCallback` will be invoked immediately. If the value returned\nis a non-empty array or object, that value will be set on the handler's\n`options.params` argument.",
"typeof": "function"
}
}, },
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
} }
@@ -20,9 +20,7 @@
}, },
"manifestTransforms": { "manifestTransforms": {
"type": "array", "type": "array",
"items": { "items": {}
"typeof": "function"
}
}, },
"maximumFileSizeToCacheInBytes": { "maximumFileSizeToCacheInBytes": {
"default": 2097152, "default": 2097152,
@@ -42,19 +40,7 @@
}, },
"exclude": { "exclude": {
"type": "array", "type": "array",
"items": { "items": {}
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
}, },
"excludeChunks": { "excludeChunks": {
"type": "array", "type": "array",
@@ -64,19 +50,7 @@
}, },
"include": { "include": {
"type": "array", "type": "array",
"items": { "items": {}
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
}, },
"mode": { "mode": {
"default": "production", "default": "production",
@@ -864,10 +838,7 @@
"onabort" "onabort"
] ]
}, },
"RouteMatchCallback": { "RouteMatchCallback": {}
"description": "The \"match\" callback is used to determine if a `Route` should apply for a\nparticular URL and request. When matching occurs in response to a fetch\nevent from the client, the `event` object is also supplied. However, since\nthe match callback can be invoked outside of a fetch event, matching logic\nshould not assume the `event` object will always be available.\nIf the match callback returns a truthy value, the matching route's\n`RouteHandlerCallback` will be invoked immediately. If the value returned\nis a non-empty array or object, that value will be set on the handler's\n`options.params` argument.",
"typeof": "function"
}
}, },
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
} }
@@ -20,9 +20,7 @@
}, },
"manifestTransforms": { "manifestTransforms": {
"type": "array", "type": "array",
"items": { "items": {}
"typeof": "function"
}
}, },
"maximumFileSizeToCacheInBytes": { "maximumFileSizeToCacheInBytes": {
"default": 2097152, "default": 2097152,
@@ -42,19 +40,7 @@
}, },
"exclude": { "exclude": {
"type": "array", "type": "array",
"items": { "items": {}
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
}, },
"excludeChunks": { "excludeChunks": {
"type": "array", "type": "array",
@@ -64,19 +50,7 @@
}, },
"include": { "include": {
"type": "array", "type": "array",
"items": { "items": {}
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
}, },
"mode": { "mode": {
"type": [ "type": [
@@ -776,10 +750,7 @@
"onabort" "onabort"
] ]
}, },
"RouteMatchCallback": { "RouteMatchCallback": {}
"description": "The \"match\" callback is used to determine if a `Route` should apply for a\nparticular URL and request. When matching occurs in response to a fetch\nevent from the client, the `event` object is also supplied. However, since\nthe match callback can be invoked outside of a fetch event, matching logic\nshould not assume the `event` object will always be available.\nIf the match callback returns a truthy value, the matching route's\n`RouteHandlerCallback` will be invoked immediately. If the value returned\nis a non-empty array or object, that value will be set on the handler's\n`options.params` argument.",
"typeof": "function"
}
}, },
"$schema": "http://json-schema.org/draft-07/schema#" "$schema": "http://json-schema.org/draft-07/schema#"
} }
+10
View File
@@ -0,0 +1,10 @@
module.exports = {
arrowParens: 'always',
bracketSpacing: false,
printWidth: 80,
quoteProps: 'consistent',
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
};