core(jsonld): add structured data validation (#6750)
This commit is contained in:
Родитель
fcd8115382
Коммит
3f15b67d23
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Recursively (DFS) traverses an object and calls provided function for each field.
|
||||
*
|
||||
* @param {*} obj
|
||||
* @param {function(string, any, Array<string>, any): void} callback
|
||||
* @param {Array<string>} path
|
||||
*/
|
||||
module.exports = function walkObject(obj, callback, path = []) {
|
||||
if (obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(obj).forEach(([fieldName, fieldValue]) => {
|
||||
const newPath = Array.from(path);
|
||||
newPath.push(fieldName);
|
||||
|
||||
callback(fieldName, fieldValue, newPath, obj);
|
||||
|
||||
if (typeof fieldValue === 'object') {
|
||||
walkObject(fieldValue, callback, newPath);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {URL} = require('../url-shim.js');
|
||||
const jsonld = require('jsonld');
|
||||
const schemaOrgContext = require('./assets/jsonldcontext.json');
|
||||
const SCHEMA_ORG_HOST = 'schema.org';
|
||||
|
||||
/**
|
||||
* Custom loader that prevents network calls and allows us to return local version of the
|
||||
* schema.org document
|
||||
* @param {string} schemaUrl
|
||||
* @param {(err: null|Error, value?: any) => void} callback
|
||||
*/
|
||||
function documentLoader(schemaUrl, callback) {
|
||||
let urlObj = null;
|
||||
|
||||
try {
|
||||
// Give a dummy base URL so relative URLs will be considered valid.
|
||||
urlObj = new URL(schemaUrl, 'http://example.com');
|
||||
} catch (e) {
|
||||
return callback(new Error('Error parsing URL: ' + schemaUrl), undefined);
|
||||
}
|
||||
|
||||
if (urlObj.host === SCHEMA_ORG_HOST && urlObj.pathname === '/') {
|
||||
callback(null, {
|
||||
document: schemaOrgContext,
|
||||
});
|
||||
} else {
|
||||
// We only process schema.org, for other schemas we return an empty object
|
||||
callback(null, {
|
||||
document: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes JSON-LD object and normalizes it by following the expansion algorithm
|
||||
* (https://json-ld.org/spec/latest/json-ld-api/#expansion).
|
||||
*
|
||||
* @param {any} inputObject
|
||||
* @returns {Promise<LH.StructuredData.ExpandedSchemaRepresentation|null>}
|
||||
*/
|
||||
module.exports = async function expand(inputObject) {
|
||||
try {
|
||||
return await jsonld.expand(inputObject, {documentLoader});
|
||||
} catch (err) {
|
||||
// jsonld wraps real errors in a bunch of junk, so see we have an underlying error first
|
||||
if (err.details && err.details.cause) throw err.details.cause;
|
||||
throw err;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const jsonlint = require('jsonlint-mod');
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @returns {{message: string, lineNumber: string|null}|null}
|
||||
*/
|
||||
module.exports = function parseJSON(input) {
|
||||
try {
|
||||
jsonlint.parse(input);
|
||||
} catch (error) {
|
||||
/** @type {string|null} */
|
||||
let line = error.at;
|
||||
|
||||
// extract line number from message
|
||||
if (!line) {
|
||||
const regexLineResult = error.message.match(/Parse error on line (\d+)/);
|
||||
|
||||
if (regexLineResult) {
|
||||
line = regexLineResult[1];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// jsonlint error message points to a specific character, but we just want the message.
|
||||
// Example:
|
||||
// ---------^
|
||||
// Unexpected character {
|
||||
let message = /** @type {string} */ (error.message);
|
||||
const regexMessageResult = error.message.match(/-+\^\n(.+)$/);
|
||||
|
||||
if (regexMessageResult) {
|
||||
message = regexMessageResult[1];
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
lineNumber: line,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const walkObject = require('./helpers/walk-object.js');
|
||||
|
||||
// This list comes from the JSON-LD 1.1 editors draft:
|
||||
// https://w3c.github.io/json-ld-syntax/#syntax-tokens-and-keywords
|
||||
const VALID_KEYWORDS = new Set([
|
||||
'@base',
|
||||
'@container',
|
||||
'@context',
|
||||
'@graph',
|
||||
'@id',
|
||||
'@index',
|
||||
'@language',
|
||||
'@list',
|
||||
'@nest',
|
||||
'@none',
|
||||
'@prefix',
|
||||
'@reverse',
|
||||
'@set',
|
||||
'@type',
|
||||
'@value',
|
||||
'@version',
|
||||
'@vocab',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {*} json
|
||||
* @return {Array<{path: string, message: string}>}
|
||||
*/
|
||||
module.exports = function validateJsonLD(json) {
|
||||
/** @type {Array<{path: string, message: string}>} */
|
||||
const errors = [];
|
||||
|
||||
walkObject(json, (name, value, path) => {
|
||||
if (name.startsWith('@') && !VALID_KEYWORDS.has(name)) {
|
||||
errors.push({
|
||||
path: path.join('/'),
|
||||
message: 'Unknown keyword',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const walkObject = require('./helpers/walk-object.js');
|
||||
const schemaStructure = require('./assets/schema-tree.json');
|
||||
const TYPE_KEYWORD = '@type';
|
||||
const SCHEMA_ORG_URL_REGEX = /https?:\/\/schema\.org\//;
|
||||
|
||||
/**
|
||||
* @param {string} uri
|
||||
* @returns {string}
|
||||
*/
|
||||
function cleanName(uri) {
|
||||
return uri.replace(SCHEMA_ORG_URL_REGEX, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
function getPropsForType(type) {
|
||||
const cleanType = cleanName(type);
|
||||
const props = schemaStructure.properties
|
||||
.filter(prop => prop.parent.includes(cleanType))
|
||||
.map(prop => prop.name);
|
||||
const foundType = findType(type);
|
||||
if (!foundType) throw new Error(`Unable to get props for missing type "${type}"`);
|
||||
const parentTypes = foundType.parent;
|
||||
|
||||
return parentTypes.reduce((allProps, type) => allProps.concat(getPropsForType(type)), props);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {{name: string, parent: Array<string>}|undefined}
|
||||
*/
|
||||
function findType(type) {
|
||||
const cleanType = cleanName(type);
|
||||
|
||||
return schemaStructure.types.find(typeObj => typeObj.name === cleanType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates keys of given object based on its type(s). Returns an array of error messages.
|
||||
*
|
||||
* @param {string|Array<string>} typeOrTypes
|
||||
* @param {Array<string>} keys
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
function validateObjectKeys(typeOrTypes, keys) {
|
||||
/** @type {Array<string>} */
|
||||
let types = [];
|
||||
|
||||
if (typeof typeOrTypes === 'string') {
|
||||
types = [typeOrTypes];
|
||||
} else if (Array.isArray(typeOrTypes)) {
|
||||
types = typeOrTypes;
|
||||
const invalidIndex = typeOrTypes.findIndex(s => typeof s !== 'string');
|
||||
if (invalidIndex >= 0) return [`Unknown value type at index ${invalidIndex}`];
|
||||
} else {
|
||||
return ['Unknown value type'];
|
||||
}
|
||||
|
||||
const unknownTypes = types.filter(t => !findType(t));
|
||||
|
||||
if (unknownTypes.length) {
|
||||
return unknownTypes
|
||||
.filter(type => SCHEMA_ORG_URL_REGEX.test(type))
|
||||
.map(type => `Unrecognized schema.org type ${type}`);
|
||||
}
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const allKnownProps = new Set();
|
||||
|
||||
types.forEach(type => {
|
||||
const knownProps = getPropsForType(type);
|
||||
|
||||
knownProps.forEach(key => allKnownProps.add(key));
|
||||
});
|
||||
|
||||
const cleanKeys = keys
|
||||
// Skip JSON-LD keywords (including invalid ones as they were already flagged in the json-ld validator)
|
||||
.filter(key => key.indexOf('@') !== 0)
|
||||
.map(key => cleanName(key));
|
||||
|
||||
return cleanKeys
|
||||
// remove Schema.org input/output constraints http://schema.org/docs/actions.html#part-4
|
||||
.map(key => key.replace(/-(input|output)$/, ''))
|
||||
.filter(key => !allKnownProps.has(key))
|
||||
.map(key => `Unexpected property "${key}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LH.StructuredData.ExpandedSchemaRepresentation|null} expandedObj Valid JSON-LD object in expanded form
|
||||
* @return {Array<{path: string, message: string}>}
|
||||
*/
|
||||
module.exports = function validateSchemaOrg(expandedObj) {
|
||||
/** @type {Array<{path: string, message: string}>} */
|
||||
const errors = [];
|
||||
|
||||
if (expandedObj === null) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
// If the array only has a single item, treat it as if it was at the root to simplify the error path.
|
||||
// Arrays longer than a single item are handled in `walkObject` below.
|
||||
if (Array.isArray(expandedObj) && expandedObj.length === 1) {
|
||||
expandedObj = expandedObj[0];
|
||||
}
|
||||
|
||||
walkObject(expandedObj, (name, value, path, obj) => {
|
||||
if (name === TYPE_KEYWORD) {
|
||||
const keyErrorMessages = validateObjectKeys(value, Object.keys(obj));
|
||||
|
||||
keyErrorMessages.forEach(message =>
|
||||
errors.push({
|
||||
// get rid of the first chunk (/@type) as it's the same for all errors
|
||||
path:
|
||||
'/' +
|
||||
path
|
||||
.slice(0, -1)
|
||||
.map(cleanName)
|
||||
.join('/'),
|
||||
message,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Call this script to update assets/jsonldcontext.json with the latest schema.org spec
|
||||
*/
|
||||
|
||||
const fetch = require('isomorphic-fetch');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const SCHEMA_ORG_URL = 'https://schema.org';
|
||||
const CONTEXT_FILE = path.join(__dirname, '../assets/jsonldcontext.json');
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const response = await fetch(SCHEMA_ORG_URL, {headers: {Accept: 'application/ld+json'}});
|
||||
const data = await response.json();
|
||||
fs.writeFileSync(CONTEXT_FILE, JSON.stringify(data, null, 2));
|
||||
console.log('Success.'); // eslint-disable-line no-console
|
||||
} catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Call this script to update assets/schema-tree.json with the latest schema.org spec
|
||||
*/
|
||||
|
||||
const fetch = require('isomorphic-fetch');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const SCHEMA_ORG_URL = 'https://schema.org/version/latest/schema.jsonld';
|
||||
const SCHEMA_TREE_FILE = path.join(__dirname, '../assets/schema-tree.json');
|
||||
|
||||
/** @typedef {import('jsonlint-mod').SchemaTreeItem} SchemaDefinition */
|
||||
|
||||
/** @typedef {import('jsonlint-mod').JSONSchemaSource} SchemaSource */
|
||||
|
||||
/** @typedef {{'@id': string}} IDRef */
|
||||
|
||||
/**
|
||||
* @param {SchemaSource} data
|
||||
*/
|
||||
function processData(data) {
|
||||
/** @type {SchemaDefinition[]} */
|
||||
const types = [];
|
||||
/** @type {SchemaDefinition[]} */
|
||||
const properties = [];
|
||||
|
||||
/** @param {string} str */
|
||||
function removePrefix(str) {
|
||||
return str.replace('http://schema.org/', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts some set of id references and returns the array of cleaned references without the `http://schema.org` prefix.
|
||||
* @param {Array<IDRef>|IDRef|undefined} parents
|
||||
*/
|
||||
function cleanIdPrefixes(parents) {
|
||||
if (Array.isArray(parents)) {
|
||||
return parents.map(item => removePrefix(item['@id']));
|
||||
} else if (parents && parents['@id']) {
|
||||
return [removePrefix(parents['@id'])];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Go through all the graph entries to find the valid types and properties.
|
||||
// i.e. this converts
|
||||
// [
|
||||
// {@id: 'http://schema.org/CafeOrCoffeeShop', @type: 'rdfs:Class', rdfs:subClassOf: {@id: 'http://schema.org/FoodEstablishment'}},
|
||||
// {@id: 'http://schema.org/bestRating', @type: 'rdfs:Property', http://schema.org/domainIncludes: {@id: 'http://schema.org/Rating'}},
|
||||
// ]
|
||||
// into
|
||||
// {
|
||||
// types: [{name: 'CafeOrCoffeeShop', parent: ['FoodEstablishment']}],
|
||||
// properties: [{name: 'bestRating', parent: ['Rating']}],
|
||||
// }
|
||||
data['@graph'].forEach(item => {
|
||||
if (item['rdfs:label'] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item['@type'] === 'rdf:Property') {
|
||||
properties.push({
|
||||
name: item['rdfs:label'],
|
||||
parent: cleanIdPrefixes(item['http://schema.org/domainIncludes']),
|
||||
});
|
||||
} else {
|
||||
types.push({
|
||||
name: item['rdfs:label'],
|
||||
parent: cleanIdPrefixes(item['rdfs:subClassOf']),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {types, properties};
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const response = await fetch(SCHEMA_ORG_URL);
|
||||
const data = await response.json();
|
||||
const processed = processData(data);
|
||||
fs.writeFileSync(SCHEMA_TREE_FILE, JSON.stringify(processed, null, 2));
|
||||
console.log('Success.'); // eslint-disable-line no-console
|
||||
} catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const parseJSON = require('./json-linter.js');
|
||||
const validateJsonLD = require('./jsonld-keyword-validator.js');
|
||||
const expandAsync = require('./json-expander.js');
|
||||
const validateSchemaOrg = require('./schema-validator.js');
|
||||
|
||||
/** @typedef {'json'|'json-ld'|'json-ld-expand'|'schema-org'} ValidatorType */
|
||||
|
||||
/**
|
||||
* Validates JSON-LD input. Returns array of error objects.
|
||||
*
|
||||
* @param {string} textInput
|
||||
* @returns {Promise<Array<{path: ?string, validator: ValidatorType, message: string}>>}
|
||||
*/
|
||||
module.exports = async function validate(textInput) {
|
||||
// STEP 1: VALIDATE JSON
|
||||
const parseError = parseJSON(textInput);
|
||||
|
||||
if (parseError) {
|
||||
return [{
|
||||
validator: 'json',
|
||||
path: parseError.lineNumber,
|
||||
message: parseError.message,
|
||||
}];
|
||||
}
|
||||
|
||||
const inputObject = JSON.parse(textInput);
|
||||
|
||||
// STEP 2: VALIDATE JSONLD
|
||||
const jsonLdErrors = validateJsonLD(inputObject);
|
||||
|
||||
if (jsonLdErrors.length) {
|
||||
return jsonLdErrors.map(error => {
|
||||
return {
|
||||
validator: /** @type {ValidatorType} */ ('json-ld'),
|
||||
path: error.path,
|
||||
message: error.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// STEP 3: EXPAND
|
||||
/** @type {LH.StructuredData.ExpandedSchemaRepresentation|null} */
|
||||
let expandedObj = null;
|
||||
try {
|
||||
expandedObj = await expandAsync(inputObject);
|
||||
} catch (error) {
|
||||
return [{
|
||||
validator: 'json-ld-expand',
|
||||
path: null,
|
||||
message: error.message,
|
||||
}];
|
||||
}
|
||||
|
||||
// STEP 4: VALIDATE SCHEMA
|
||||
const schemaOrgErrors = validateSchemaOrg(expandedObj);
|
||||
|
||||
if (schemaOrgErrors.length) {
|
||||
return schemaOrgErrors.map(error => {
|
||||
return {
|
||||
validator: /** @type {ValidatorType} */ ('schema-org'),
|
||||
path: error.path,
|
||||
message: error.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/* eslint-env jest */
|
||||
|
||||
const assert = require('assert');
|
||||
const validateJSONLD = require('../../lib/sd-validation/sd-validation.js');
|
||||
|
||||
describe('JSON validation', () => {
|
||||
it('reports missing closing bracket', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"test": "test"
|
||||
`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].path, 2);
|
||||
assert.ok(errors[0].message.indexOf(`Expecting '}'`) === 0);
|
||||
});
|
||||
|
||||
it('reports missing comma', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"test": "test"
|
||||
"test2": "test2"
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].path, 2);
|
||||
assert.ok(errors[0].message.indexOf(`Expecting 'EOF', '}', ':', ',', ']'`) === 0);
|
||||
});
|
||||
|
||||
it('reports duplicated property', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"test": "test",
|
||||
"test2": {
|
||||
"test2-1": "test",
|
||||
"test2-1": "test2"
|
||||
}
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.ok(errors[0].message, `Duplicate key 'test2-1'`);
|
||||
});
|
||||
|
||||
it('parses valid json', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"test": "test",
|
||||
"test2": {
|
||||
"test2-1": "test",
|
||||
"test2-2": "test2"
|
||||
},
|
||||
"test3": null,
|
||||
"test4": 123,
|
||||
"test5": [1,2,3]
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON-LD validation', () => {
|
||||
it('reports unknown keywords', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@type": {},
|
||||
"@context": {},
|
||||
"@test": {}
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].message, 'Unknown keyword');
|
||||
assert.equal(errors[0].path, '@test');
|
||||
});
|
||||
|
||||
it('reports invalid context', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": {"x":"x"}
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.ok(errors[0].message.indexOf('@context terms must define an @id') !== -1);
|
||||
});
|
||||
|
||||
it('reports invalid keyword value', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": "http://schema.org/",
|
||||
"@type": 23
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.ok(errors[0].message.indexOf('"@type" value must a string') !== -1);
|
||||
});
|
||||
|
||||
it('reports invalid id value', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": {
|
||||
"image": {
|
||||
"@id": "@error"
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.ok(errors[0].message.indexOf('@id value must be an absolute IRI') !== -1);
|
||||
});
|
||||
|
||||
it('reports invalid context URL', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": "http://"
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].message, 'Error parsing URL: http://');
|
||||
});
|
||||
});
|
||||
|
||||
describe('schema.org validation', () => {
|
||||
it('reports unknown types', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Cat"
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].message, 'Unrecognized schema.org type http://schema.org/Cat');
|
||||
});
|
||||
|
||||
it('handles arrays of json schemas', async () => {
|
||||
const errors = await validateJSONLD(`[
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Cat"
|
||||
},
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Dog"
|
||||
}
|
||||
]`);
|
||||
|
||||
assert.equal(errors.length, 2);
|
||||
assert.equal(errors[0].message, 'Unrecognized schema.org type http://schema.org/Cat');
|
||||
assert.equal(errors[1].message, 'Unrecognized schema.org type http://schema.org/Dog');
|
||||
});
|
||||
|
||||
it('reports unknown types for objects with multiple types', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": "http://schema.org",
|
||||
"@type": ["Article", "Dog"]
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].message, 'Unrecognized schema.org type http://schema.org/Dog');
|
||||
});
|
||||
|
||||
it('reports unexpected fields', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"author": "Cat",
|
||||
"datePublished": "Oct 29th 2017",
|
||||
"dateModified": "Oct 29th 2017",
|
||||
"headline": "Human's New Best Friend - Cat",
|
||||
"image": "https://cats.rock/cat.bmp",
|
||||
"publisher": "Cat Magazine",
|
||||
"mainEntityOfPage": "https://cats.rock/magazine.html",
|
||||
"controversial": true
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 1);
|
||||
assert.equal(errors[0].message, 'Unexpected property "controversial"');
|
||||
});
|
||||
|
||||
it('passes if non-schema.org context', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": "http://www.w3.org/ns/activitystreams",
|
||||
"@type": "Create",
|
||||
"actor": {
|
||||
"@type": "Person",
|
||||
"@id": "acct:sally@example.org",
|
||||
"displayName": "Sally"
|
||||
},
|
||||
"object": {
|
||||
"@type": "Note",
|
||||
"content": "This is a simple note"
|
||||
},
|
||||
"published": "2015-01-25T12:34:56Z"
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 0);
|
||||
});
|
||||
|
||||
it('passes if everything is OK', async () => {
|
||||
const errors = await validateJSONLD(`{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Article",
|
||||
"author": "Cat",
|
||||
"datePublished": "Oct 29th 2017",
|
||||
"dateModified": "Oct 29th 2017",
|
||||
"headline": "Human's New Best Friend - Cat",
|
||||
"image": "https://cats.rock/cat.bmp",
|
||||
"publisher": "Cat Magazine",
|
||||
"mainEntityOfPage": "https://cats.rock/magazine.html"
|
||||
}`);
|
||||
|
||||
assert.equal(errors.length, 0);
|
||||
});
|
||||
});
|
|
@ -113,6 +113,7 @@
|
|||
"glob": "^7.1.3",
|
||||
"idb-keyval": "2.2.0",
|
||||
"intl": "^1.2.5",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"jest": "^24.3.0",
|
||||
"jsdom": "^12.2.0",
|
||||
"make-dir": "^1.3.0",
|
||||
|
@ -140,6 +141,8 @@
|
|||
"intl-messageformat-parser": "^1.4.0",
|
||||
"jpeg-js": "0.1.2",
|
||||
"js-library-detector": "^5.1.0",
|
||||
"jsonld": "^1.5.0",
|
||||
"jsonlint-mod": "^1.7.4",
|
||||
"lighthouse-logger": "^1.2.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lookup-closest-locale": "6.0.4",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
declare module 'isomorphic-fetch' {
|
||||
// Just reuse the types from the built-in window.fetch
|
||||
export = window.fetch
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
declare module 'jsonld' {
|
||||
type CallbackFn = (err: null|Error, result?: any) => void
|
||||
|
||||
interface JsonldOptions {
|
||||
documentLoader: (url: string, callback: CallbackFn) => void
|
||||
}
|
||||
|
||||
export function expand(object: any, options: JsonldOptions): Promise<any>;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
declare module 'jsonlint-mod' {
|
||||
export function parse(input: string): unknown;
|
||||
|
||||
interface SchemaIDReference {
|
||||
'@id': string;
|
||||
}
|
||||
|
||||
interface SchemaSourceItem {
|
||||
'@id': string;
|
||||
'@type': string;
|
||||
'rdfs:label'?: string;
|
||||
'rdfs:subClassOf'?: SchemaIDReference | SchemaIDReference[];
|
||||
'http://schema.org/domainIncludes'?: SchemaIDReference | SchemaIDReference[];
|
||||
}
|
||||
|
||||
interface SchemaTreeItem {
|
||||
name: string;
|
||||
parent: string[];
|
||||
}
|
||||
|
||||
export interface JSONSchemaSource {
|
||||
'@graph': SchemaSourceItem[];
|
||||
}
|
||||
|
||||
export interface JSONSchemaTree {
|
||||
types: SchemaTreeItem[];
|
||||
properties: SchemaTreeItem[];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* @license Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
declare global {
|
||||
module LH {
|
||||
module StructuredData {
|
||||
export interface ExpandedSchemaRepresentationItem {
|
||||
[schemaRef: string]: Array<
|
||||
string |
|
||||
{
|
||||
'@id'?: string;
|
||||
'@type'?: string;
|
||||
'@value'?: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export type ExpandedSchemaRepresentation =
|
||||
| Array<ExpandedSchemaRepresentationItem>
|
||||
| ExpandedSchemaRepresentationItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// empty export to keep file a module
|
||||
export {};
|
99
yarn.lock
99
yarn.lock
|
@ -751,6 +751,11 @@ JSONStream@^1.0.4:
|
|||
jsonparse "^1.2.0"
|
||||
through ">=2.2.7 <3"
|
||||
|
||||
"JSV@>= 4.0.x":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
|
||||
integrity sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=
|
||||
|
||||
abab@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
|
||||
|
@ -930,6 +935,11 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
|
|||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
ansi-styles@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
|
||||
integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=
|
||||
|
||||
anymatch@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
|
||||
|
@ -1849,6 +1859,15 @@ chalk@^2.4.2:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
|
||||
integrity sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=
|
||||
dependencies:
|
||||
ansi-styles "~1.0.0"
|
||||
has-color "~0.1.0"
|
||||
strip-ansi "~0.1.0"
|
||||
|
||||
chardet@^0.4.0:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
|
||||
|
@ -3938,6 +3957,11 @@ has-ansi@^2.0.0:
|
|||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
has-color@~0.1.0:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
|
||||
integrity sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=
|
||||
|
||||
has-flag@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
|
||||
|
@ -4684,6 +4708,14 @@ isobject@^3.0.0, isobject@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||
|
||||
isomorphic-fetch@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
|
||||
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
|
||||
dependencies:
|
||||
node-fetch "^1.0.1"
|
||||
whatwg-fetch ">=0.10.0"
|
||||
|
||||
isstream@0.1.x, isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
|
@ -5298,6 +5330,24 @@ jsonify@~0.0.0:
|
|||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
|
||||
|
||||
jsonld@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-1.5.0.tgz#c9f4d1680ab18530f760e5685eead25251451943"
|
||||
integrity sha512-7jF9WXK4nuHvhz/qT6A4DEZ58tUYgrV98xBJEgHFhQ6GQaNT+oU1zqkFXKtDZsKsiEs/1K/VShNnat6SISb3jg==
|
||||
dependencies:
|
||||
rdf-canonize "^1.0.1"
|
||||
request "^2.88.0"
|
||||
semver "^5.6.0"
|
||||
xmldom "0.1.19"
|
||||
|
||||
jsonlint-mod@^1.7.4:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/jsonlint-mod/-/jsonlint-mod-1.7.4.tgz#310390e1a6a85cef99f45f200e662ef23b48f7a6"
|
||||
integrity sha512-FYOkwHqiuBbdVCHgXYlmtL+iUOz9AxCgjotzXl+edI0Hc1km1qK6TrBEAyPpO+5R0/IX3XENRp66mfob4jwxow==
|
||||
dependencies:
|
||||
JSV ">= 4.0.x"
|
||||
nomnom ">= 1.5.x"
|
||||
|
||||
jsonparse@^1.2.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
|
||||
|
@ -5969,11 +6019,24 @@ node-fetch@1.6.3:
|
|||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
|
||||
dependencies:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-fetch@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"
|
||||
integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==
|
||||
|
||||
node-forge@^0.7.6:
|
||||
version "0.7.6"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
|
||||
integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
|
||||
|
||||
node-int64@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
||||
|
@ -5994,6 +6057,14 @@ node-notifier@^5.2.1:
|
|||
shellwords "^0.1.1"
|
||||
which "^1.3.0"
|
||||
|
||||
"nomnom@>= 1.5.x":
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
|
||||
integrity sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=
|
||||
dependencies:
|
||||
chalk "~0.4.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
normalize-package-data@^2.3.0, normalize-package-data@^2.3.5:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
|
||||
|
@ -6810,6 +6881,14 @@ rc@^1.0.1, rc@^1.1.6:
|
|||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
rdf-canonize@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-1.0.1.tgz#0a200a12c169e0008dd10e5b2b3855a55db0626c"
|
||||
integrity sha512-vQq6q7BIUwrVQijKRYdunxlodkn0Btjv2MnJ4S3rOUELsghq7fGuDaWuqBNbXca3KRbcRS6HwTIT2hJbJej2UA==
|
||||
dependencies:
|
||||
node-forge "^0.7.6"
|
||||
semver "^5.6.0"
|
||||
|
||||
read-only-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
|
||||
|
@ -7788,6 +7867,11 @@ strip-ansi@^5.0.0:
|
|||
dependencies:
|
||||
ansi-regex "^4.0.0"
|
||||
|
||||
strip-ansi@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
|
||||
integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=
|
||||
|
||||
strip-bom@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794"
|
||||
|
@ -8211,6 +8295,11 @@ undeclared-identifiers@^1.1.2:
|
|||
simple-concat "^1.0.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
underscore@~1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
|
||||
integrity sha1-izixDKze9jM3uLJOT/htRa6lKag=
|
||||
|
||||
union-value@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
|
||||
|
@ -8416,6 +8505,11 @@ whatwg-fetch@2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.1.tgz#078b9461bbe91cea73cbce8bb122a05f9e92b772"
|
||||
integrity sha1-B4uUYbvpHOpzy86LsSKgX56St3I=
|
||||
|
||||
whatwg-fetch@>=0.10.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
|
||||
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
|
||||
|
||||
whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4"
|
||||
|
@ -8617,6 +8711,11 @@ xmlchars@^1.3.1:
|
|||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-1.3.1.tgz#1dda035f833dbb4f86a0c28eaa6ca769214793cf"
|
||||
integrity sha512-tGkGJkN8XqCod7OT+EvGYK5Z4SfDQGD30zAa58OcnAa0RRWgzUEK72tkXhsX1FZd+rgnhRxFtmO+ihkp8LHSkw==
|
||||
|
||||
xmldom@0.1.19:
|
||||
version "0.1.19"
|
||||
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
|
||||
integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw=
|
||||
|
||||
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
|
|
Загрузка…
Ссылка в новой задаче