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) {
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) {
schema.properties.manifestTransforms.items = {typeof: 'function'};
schema.properties.manifestTransforms.items = {};
}
if (schema.properties.exclude) {
schema.properties.exclude.items.anyOf = [
{'$ref': '#/definitions/RegExp'},
{type: 'string'},
{typeof: 'function'},
];
schema.properties.exclude.items = {};
}
if (schema.properties.include) {
schema.properties.include.items.anyOf = [
{'$ref': '#/definitions/RegExp'},
{type: 'string'},
{typeof: 'function'},
];
schema.properties.include.items = {};
}
if (schema.definitions.RouteMatchCallback) {
delete schema.definitions.RouteMatchCallback.type;
delete schema.definitions.RouteMatchCallback.additionalProperties;
schema.definitions.RouteMatchCallback.typeof = 'function';
schema.definitions.RouteMatchCallback = {};
}
await fse.writeJSON(upath.join(packagePath, 'src', 'schema',
`${optionType}.json`), schema, {spaces: 2});
}
-1
View File
@@ -29,7 +29,6 @@
"@rollup/plugin-replace": "^2.4.1",
"@surma/rollup-plugin-off-main-thread": "^1.4.1",
"ajv": "^8.6.0",
"ajv-keywords": "^5.0.0",
"common-tags": "^1.8.0",
"fast-json-stable-stringify": "^2.1.0",
"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.`,
'cache-name-required': ol`When using cache expiration, you must also
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 {oneLine as ol} from 'common-tags';
import Ajv, {JSONSchemaType} from 'ajv';
import ajvKeywords from 'ajv-keywords';
import {errors} from './errors';
@@ -21,15 +20,18 @@ import {
WebpackInjectManifestOptions,
} from '../types';
type MethodNames = 'GenerateSW' | 'GetManifest' | 'InjectManifest' |
'WebpackGenerateSW' | 'WebpackInjectManifest';
type MethodNames =
| 'GenerateSW'
| 'GetManifest'
| 'InjectManifest'
| 'WebpackGenerateSW'
| 'WebpackInjectManifest';
const ajv = new Ajv({
useDefaults: true,
});
ajvKeywords(ajv, ['typeof']);
const DEFAULT_EXCLUDE_VALUE= [/\.map$/, /^manifest.*\.js$/];
const DEFAULT_EXCLUDE_VALUE = [/\.map$/, /^manifest.*\.js$/];
export class WorkboxConfigError extends Error {
constructor(message?: string) {
@@ -41,10 +43,12 @@ export class WorkboxConfigError extends Error {
function validate<T>(input: unknown, methodName: MethodNames): T {
// Don't mutate input: https://github.com/GoogleChrome/workbox/issues/2158
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 validate = ajv.compile(jsonSchema);
if (validate(inputCopy)) {
// All methods support manifestTransforms, so validate it here.
ensureValidManifestTransforms(inputCopy);
return inputCopy;
}
@@ -56,19 +60,42 @@ function validate<T>(input: unknown, methodName: MethodNames): T {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
schema: jsonSchema as any,
});
const messages = betterErrors.map((err) => ol`[${err.path}] ${err.message}.
${err.suggestion ? err.suggestion : ''}`);
const messages = betterErrors.map(
(err) => ol`[${err.path}] ${err.message}.
${err.suggestion ? err.suggestion : ''}`,
);
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(
options: GenerateSWOptions | WebpackGenerateSWOptions,
): void {
if (options.navigationPreload &&
(!Array.isArray(options.runtimeCaching) ||
options.runtimeCaching.length === 0)) {
throw new WorkboxConfigError(errors['nav-preload-runtime-caching']);
if (
options.navigationPreload &&
(!Array.isArray(options.runtimeCaching) ||
options.runtimeCaching.length === 0)
) {
throw new WorkboxConfigError(errors['nav-preload-runtime-caching']);
}
}
@@ -76,7 +103,10 @@ function ensureValidCacheExpiration(
options: GenerateSWOptions | WebpackGenerateSWOptions,
): void {
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']);
}
}
@@ -85,10 +115,14 @@ function ensureValidCacheExpiration(
function ensureValidRuntimeCachingOrGlobDirectory(
options: GenerateSWOptions,
): void {
if (!options.globDirectory &&
(!Array.isArray(options.runtimeCaching) ||
options.runtimeCaching.length === 0)) {
throw new WorkboxConfigError(errors['no-manifest-entries-or-runtime-caching']);
if (
!options.globDirectory &&
(!Array.isArray(options.runtimeCaching) ||
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);
ensureValidCacheExpiration(validatedOptions);
ensureValidRuntimeCachingOrGlobDirectory(validatedOptions);
return validatedOptions;
}
@@ -104,27 +139,47 @@ export function validateGetManifestOptions(input: unknown): GetManifestOptions {
return validate<GetManifestOptions>(input, 'GetManifest');
}
export function validateInjectManifestOptions(input: unknown): InjectManifestOptions {
export function validateInjectManifestOptions(
input: unknown,
): InjectManifestOptions {
return validate<InjectManifestOptions>(input, 'InjectManifest');
}
// The default `exclude: [/\.map$/, /^manifest.*\.js$/]` value can't be
// represented in the JSON schema, so manually set it for the webpack options.
export function validateWebpackGenerateSWOptions(input: unknown): WebpackGenerateSWOptions {
const inputWithExcludeDefault = Object.assign({
// Make a copy, as exclude can be mutated when used.
exclude: Array.from(DEFAULT_EXCLUDE_VALUE),
}, input);
const validatedOptions = validate<WebpackGenerateSWOptions>(inputWithExcludeDefault, 'WebpackGenerateSW');
export function validateWebpackGenerateSWOptions(
input: unknown,
): WebpackGenerateSWOptions {
const inputWithExcludeDefault = Object.assign(
{
// 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);
ensureValidCacheExpiration(validatedOptions);
return validatedOptions;
}
export function validateWebpackInjectManifestOptions(input: unknown): WebpackInjectManifestOptions {
const inputWithExcludeDefault = Object.assign({
// Make a copy, as exclude can be mutated when used.
exclude: Array.from(DEFAULT_EXCLUDE_VALUE),
}, input);
return validate<WebpackInjectManifestOptions>(inputWithExcludeDefault, 'WebpackInjectManifest');
export function validateWebpackInjectManifestOptions(
input: unknown,
): WebpackInjectManifestOptions {
const inputWithExcludeDefault = Object.assign(
{
// 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": {
"type": "array",
"items": {
"typeof": "function"
}
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"default": 2097152,
@@ -861,10 +859,7 @@
"onabort"
]
},
"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"
}
"RouteMatchCallback": {}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
@@ -20,9 +20,7 @@
},
"manifestTransforms": {
"type": "array",
"items": {
"typeof": "function"
}
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"default": 2097152,
@@ -753,10 +751,7 @@
"onabort"
]
},
"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"
}
"RouteMatchCallback": {}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
@@ -20,9 +20,7 @@
},
"manifestTransforms": {
"type": "array",
"items": {
"typeof": "function"
}
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"default": 2097152,
@@ -765,10 +763,7 @@
"onabort"
]
},
"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"
}
"RouteMatchCallback": {}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
@@ -20,9 +20,7 @@
},
"manifestTransforms": {
"type": "array",
"items": {
"typeof": "function"
}
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"default": 2097152,
@@ -42,19 +40,7 @@
},
"exclude": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
"items": {}
},
"excludeChunks": {
"type": "array",
@@ -64,19 +50,7 @@
},
"include": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
"items": {}
},
"mode": {
"default": "production",
@@ -864,10 +838,7 @@
"onabort"
]
},
"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"
}
"RouteMatchCallback": {}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
@@ -20,9 +20,7 @@
},
"manifestTransforms": {
"type": "array",
"items": {
"typeof": "function"
}
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"default": 2097152,
@@ -42,19 +40,7 @@
},
"exclude": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
"items": {}
},
"excludeChunks": {
"type": "array",
@@ -64,19 +50,7 @@
},
"include": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"type": "string"
},
{
"typeof": "function"
}
]
}
"items": {}
},
"mode": {
"type": [
@@ -776,10 +750,7 @@
"onabort"
]
},
"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"
}
"RouteMatchCallback": {}
},
"$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',
};