diff --git a/.taskcluster.yml b/.taskcluster.yml index e5e6807..5253943 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -11,7 +11,7 @@ tasks: - push payload: maxRunTime: 3600 - image: node:8 + image: node:10 command: - /bin/bash - '--login' diff --git a/README.md b/README.md index 8f14ab9..1de0386 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ including manifests, API references, exchange references, and JSON schemas. It exports a class, `References`, that manages reading and writing this data in several formats, as well as performing transformations and consistency checks. -It also serves as the canonical repository for a few miscellaneous and -historical schemas. +It also serves as the canonical repository for a few shared schemas and +meta-schemas under `schemas/`. # Data Formats @@ -32,17 +32,12 @@ during the cluster build process: This is a subset of the [taskcluster-lib-docs](https://github.com/taskcluster/taskcluster-lib-docs) documentation tarball format, and `metadata.json` is defined there. The -library will load `exchanges.json` as an alternative to `events.json`, if -present. +library will in fact load any `.json` files in `references` and interpret them +according to their schema. So, the `$schema` property of every file in +`references` must point to the relevant schema using a `/`-relative URI such as +`/schemas/common/api-reference-v1.json`. -TODO: - * events/api.json - * $schema uri format - * references to schemas - * schemas - * $ref must be relative - -*NOTE* the library can only read this format, not write. +*NOTE* this library can only read data in this format, not write it. ## URI-Structured @@ -70,57 +65,107 @@ Taskcluster rootUrl. └── ... ``` -TODO: - * add a single-JSON-file format +## Serializable + +The serializable format is similar to the URI-structured format, but as a +JSON-serializable data structure consisting of an array of `{filename, +content}` objects. + +```json +[ + { + "filename": "references/manifest.json", + "content": { + "$schema": ... + } + }, ... +] +``` # Abstract and Absolute References -A URI-Structured representation of References can either be "abstract" or -"absolute". The abstract form contains URIs of the form -`taskcluster:/references/..` and `taskcluster:/schemas/..`, abstract from any -specific rootUrl. The absolute form contains URIs that incorporate a specific -rootUrl and thus could actually be fetched with an HTTP GET (in other words, -the URIs are URLs). +A References instance can either be "abstract" or +"absolute". The abstract form contains `/`-relative URIs of the form +`/references/..` and `/schemas/..`, abstract from any specific rootUrl. These +appear anywhere a link to another object in the references or schemas appears, +including `$schema`, `$id`, and `$ref`. -The library can load either form, but an in-memory References instance is -always abstract. Writing an absolute form always requires a rootUrl. +The absolute form contains URIs that incorporate a specific rootUrl and thus +could actually be fetched with an HTTP GET (in other words, the URIs are URLs). -# File Contents +The library can load and represnt either form, converting between them with +`asAbsolute` and `asAbstract`. Some operations are only valid on one form. +For example, json-schema requires that `$schema` properties be absolute URLs, +so schema operations on schemas should not be performed on the abstract form. -## Manifests +# API -The `references/manifest.json` file contains a JSON document with the following -structure (documented in `manifest-vN.json` as linked from its `$schema` -property): +To create a References instance, use one of the following methods: -```json -{ - "services": [ - { - "serviceName": "someservice", - "apis": [ - {"version": "v1", "reference": "/references/someservice/v1/api.json"} - ], - "pulse": [ - {"version": "v2", "reference": "/references/fake/v2/exchanges.json"} - ] - } - ] -} +```js +const References = require('taskcluster-lib-references'); + +// Build from a built services format (which is always abstract) +references = References.fromBuiltServices({directory: '/build/directory'}); + +// Build from a uri-structured format; omit rootUrl when on-disk data is abstract +references = References.fromUriStructured({directory: '/app', rootUrl}); + +// Build from a serializable data structure; omit rootUrl if data is abstract +references = References.fromSerializable({serializable, rootUrl}); +``` + +To validate the references, call `references.validate()`. +If validation fails, the thrown error will have a `problems` property containing the discovered problems. + +To convert between abstract and absolute references, use + +```js +const abstract = references.asAbstract(); +const absolute = abstract.asAbsolute(rootUrl); ``` -The services are listed, each with a name and links to the api references and pulse (event) references. -The links (`reference`) are relative to the rootUrl. +Note that both of these operations create a new object; Reference instances are +considered immutable. + +To write data back out, use + +```js +// write URI-structured data +references.writeUriStructured(directory); + +// create a serializable data structure +data = references.makeSerializable(); +``` -## Schemas +To build an [Ajv](https://github.com/epoberezkin/ajv) instance containing all schemas and metaschemas, call `references.makeAjv()`. +This is only valid on an absolute References instance. -Schemas are available at `schemas///.json` in the URI-structured format. -The the schema's `$id` contains this path. +To get a specific schema, call `references.getSchema($id)`, with an absolute or abstract `$id` as appropriate. -## Reference Documents +# Validation + +Validation occurs automatically in most operations on this class. + +Every schema must: +* have a valid `$id` within a Taskcluster deployment (so, a relative path of `/schemas//.json#`); +* have a valid `$schema`, either a json-schema.org URL or another schema in the references; and +* validate against the schema indicated by `$schema`. + +All `$ref` links in schemas must be relative and must refer only to schemas in +the same service (so `v1/more-data.json` may refer to +`../util/cats.json#definitions/jellicle` but not to +`/schemas/anotherservice/v1/cats.json`). As an exception, refs may refer to +`http://json-schema.org` URIs. + +Every reference must: +* have a valid `$schema`, defined in the references; +* have a schema that, in turn, hsa metaschema `/schemas/common/metadata-metaschema.json#`; +* validate against the schema indicated by `$schema`. + +# File Contents -Reference documents, as generated by `taskcluster-lib-api` and `pulse-publisher`, are available at the URLs referenced from `manifest.json`. -These are the same as the URLs generated by `taskcluster-lib-urls`' `apiReference` and `exchangeReference` functions. +The contents of the files handled by this library are described in the schemas under `schemas/` in the source repository, or under `/schemas/common` in a Taskcluster deployment. # Development diff --git a/package.json b/package.json index 872ca0b..5d64e76 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,13 @@ "main": "src/index.js", "engine-strict": true, "engines": { - "node": "^8.0.0", + "node": "^10.0.0", "yarn": "^1.0.0" }, + "files": [ + "src", + "schemas" + ], "scripts": { "lint": "eslint src/*.js test/*.js", "test": "mocha test/*_test.js", @@ -16,12 +20,13 @@ "author": "Dustin J. Mitchell ", "license": "MPL-2.0", "dependencies": { + "ajv": "^6.5.5", + "js-yaml": "^3.12.0", "lodash": "^4.17.10", "mkdirp": "^0.5.1", + "regex-escape": "^3.4.8", "rimraf": "^2.6.2", - "taskcluster-lib-urls": "^10.0.0", - "taskcluster-lib-validate": "^11.0.2", - "walk": "^2.3.13" + "taskcluster-lib-urls": "^11.0.0" }, "devDependencies": { "eslint-config-taskcluster": "^3.1.0", diff --git a/schemas/action-schema-v1.yml b/schemas/action-schema-v1.yml index 1ba4016..c69d063 100644 --- a/schemas/action-schema-v1.yml +++ b/schemas/action-schema-v1.yml @@ -1,4 +1,5 @@ $schema: http://json-schema.org/draft-06/schema# +$id: "/schemas/common/action-schema-v1.json#" title: Schema for Exposing Actions description: | This document specifies the schema for the `public/actions.json` used by diff --git a/schemas/api-reference-v0.yml b/schemas/api-reference-v0.yml index 8610d8e..6db2a35 100644 --- a/schemas/api-reference-v0.yml +++ b/schemas/api-reference-v0.yml @@ -1,6 +1,10 @@ -$schema: 'http://json-schema.org/draft-06/schema#' +$schema: "/schemas/common/metadata-metaschema.json#" +$id: "/schemas/common/api-reference-v0.json#" title: API Reference File description: Reference of methods implemented by API +metadata: + name: api + version: 0 type: object definitions: scopeExpressionTemplateString: @@ -79,7 +83,7 @@ definitions: additionalProperties: false properties: version: - description: API reference version + description: (deprecated) type: integer enum: - 0 @@ -237,7 +241,7 @@ properties: - description additionalProperties: false required: - - version + - apiVersion - $schema - title - description diff --git a/schemas/exchanges-reference-v0.yml b/schemas/exchanges-reference-v0.yml index a90e598..bf52b7d 100644 --- a/schemas/exchanges-reference-v0.yml +++ b/schemas/exchanges-reference-v0.yml @@ -1,12 +1,20 @@ -$schema: 'http://json-schema.org/draft-06/schema#' +$schema: "/schemas/common/metadata-metaschema.json#" +$id: "/schemas/common/exchanges-reference-v0.json#" title: Exchange Reference File description: Reference of exchanges published +metadata: + name: exchanges + version: 0 type: object properties: version: - description: Exchange reference version + description: (deprecated) enum: - 0 + apiVersion: + description: Version of the API + type: string + pattern: '^v[0-9]+$' $schema: description: >- Link to schema for this reference. That is a link to this very document. @@ -107,7 +115,7 @@ properties: - schema additionalProperties: false required: - - version + - apiVersion - $schema - title - description diff --git a/schemas/manifest-v2.yml b/schemas/manifest-v2.yml index 27ed29f..2353cb5 100644 --- a/schemas/manifest-v2.yml +++ b/schemas/manifest-v2.yml @@ -1,10 +1,9 @@ $schema: http://json-schema.org/draft-06/schema# +$id: "/schemas/common/manifest-v2.json#" title: "Taskcluster Service Manifest" description: |- Manifest of taskcluster service definitions available in a taskcluster service deployment. These manifests are served from `$ROOT_URL/references/manifest.json`. - - See https://github.com/taskcluster/taskcluster-references for further information. type: object properties: services: diff --git a/schemas/manifest-v3.yml b/schemas/manifest-v3.yml new file mode 100644 index 0000000..86ccd4e --- /dev/null +++ b/schemas/manifest-v3.yml @@ -0,0 +1,20 @@ +$schema: "/schemas/common/metadata-metaschema.json#" +$id: "/schemas/common/manifest-v3.json#" +title: "Taskcluster Service Manifest" +description: |- + Manifest of taskcluster service definitions available in a taskcluster service deployment. + These manifests are served from `$ROOT_URL/references/manifest.json`. +metadata: + name: manifest + version: 3 +type: object +properties: + references: + type: array + description: "Array of URLs of reference documents" + items: + type: string + formt: uri +additionalProperties: false +required: + - references diff --git a/schemas/metadata-metaschema.yml b/schemas/metadata-metaschema.yml new file mode 100644 index 0000000..66b3c5c --- /dev/null +++ b/schemas/metadata-metaschema.yml @@ -0,0 +1,35 @@ +$schema: "http://json-schema.org/draft-06/schema#" +$id: "/schemas/common/metadata-metaschema.json#" +title: "JSON-Schema Meta-Schema, with the addition of a `metadata` property" +allOf: + - {$ref: "http://json-schema.org/draft-06/schema#"} + - type: object + properties: + metadata: + title: "Metadata for this schema" + description: | + Metadata identifying the documents that the schema document describes, + giving both a name (a category of document) and a version (to allow + several versions of the same category). Consumers of the documents can + consult the schema metadata to determine how to process the document. + + Any changes to a schema that require changes to consumers of the described + documents should be accompanied by a version increase. + type: object + properties: + name: + title: "Name of the document category" + description: | + This is used to identify the category of document for later consumption. + It is also used to determine schema id's. Common values for Taskcluster + references are `manifest`, `exchanges`, and `api`. + type: string + version: + title: "Version of the document format" + type: integer + additionalProperties: false + required: + - version + - name + required: + - metadata diff --git a/src/built-services.js b/src/built-services.js new file mode 100644 index 0000000..3dc09d3 --- /dev/null +++ b/src/built-services.js @@ -0,0 +1,80 @@ +const path = require('path'); +const util = require('util'); +const fs = require('fs'); + +/** + * Read all references for this service and add them to the array of references. + * + * Note that nothing is determined from the filename - the reference itself contains + * enough data to determine its eventual URL. + */ +const loadReferences = (serviceDirectory, references) => { + const referencesDir = path.join(serviceDirectory, 'references'); + if (!fs.existsSync(referencesDir)) { + return; + } + for (let filename of fs.readdirSync(referencesDir)) { + filename = path.join(referencesDir, filename); + const data = fs.readFileSync(filename); + const content = JSON.parse(data); + + references.push({filename, content}); + } +}; + +/** + * Read all schemas for this service and add them to the array of schemas + * + * Note that the schema filenames are ignored - the schema's $id is enough to determine + * its eventual URL. + */ +const loadSchemas = (serviceDirectory, schemas) => { + const schemasDir = path.join(serviceDirectory, 'schemas'); + if (!fs.existsSync(schemasDir)) { + return; + } + + const queue = [schemasDir]; + while (queue.length) { + const filename = queue.shift(); + const st = fs.lstatSync(filename); + if (st.isDirectory()) { + for (let dentry of fs.readdirSync(filename)) { + queue.push(path.join(filename, dentry)); + } + } else { + schemas.push({ + filename, + content: JSON.parse(fs.readFileSync(filename)), + }); + } + } +}; + +/** + * Load all schemas and references from `directory`. + */ +const load = ({directory}) => { + const references = []; + const schemas = []; + + fs.readdirSync(directory).forEach(dentry => { + const filename = path.join(directory, dentry); + if (!fs.lstatSync(filename).isDirectory()) { + throw new Error(`${filename} is not a directory`); + } + + // load the metadata and check the version + const metadata = JSON.parse(fs.readFileSync(path.join(filename, 'metadata.json'))); + if (metadata.version !== 1) { + throw new Error(`${dentry}: unrecognized metadata version`); + } + + loadReferences(filename, references); + loadSchemas(filename, schemas); + }); + + return {references, schemas}; +}; + +exports.load = load; diff --git a/src/common-schemas.js b/src/common-schemas.js new file mode 100644 index 0000000..f95c457 --- /dev/null +++ b/src/common-schemas.js @@ -0,0 +1,37 @@ +const yaml = require('js-yaml'); +const path = require('path'); +const util = require('util'); +const fs = require('fs'); +const assert = require('assert'); + +let _commonSchemas; + +/** + * Read the common schemas from this library's schemas/ directory. Note + * that this differs slightly from services' schemas/ directories, in that + * the files each contain an (abstract) $id, cannot use $const, and are free + * to $ref anything they like -- all things taskcluster-lib-validate does not + * allow for services. + */ +const getCommonSchemas = () => { + if (_commonSchemas) { + return _commonSchemas; + } + + _commonSchemas = []; + const dir = path.join(__dirname, '..', 'schemas'); + for (let dentry of fs.readdirSync(dir)) { + if (!dentry.endsWith('.yml')) { + continue; + } + const filename = path.join(dir, dentry); + const data = fs.readFileSync(filename); + const content = yaml.safeLoad(data); + assert(content.$id, `${filename} has no $id`); + assert(content.$schema, `${filename} has no $id`); + _commonSchemas.push({content, filename: `schemas/${dentry}`}); + } + return _commonSchemas; +}; + +exports.getCommonSchemas = getCommonSchemas; diff --git a/src/index.js b/src/index.js index ddd7654..703be66 100644 --- a/src/index.js +++ b/src/index.js @@ -1,2 +1,3 @@ -export default class References { -} +const {References} = require('./references'); + +module.exports = References; diff --git a/src/load.js b/src/load.js deleted file mode 100644 index 288ec18..0000000 --- a/src/load.js +++ /dev/null @@ -1,95 +0,0 @@ -const path = require('path'); -const util = require('util'); -const {walk} = require('walk'); -const fs = require('fs'); - -// fs.promises is only available in Node 10+, so manually promisify things: -const readdir = util.promisify(fs.readdir); -const lstat = util.promisify(fs.lstat); -const exists = util.promisify(fs.exists); -const readFile = util.promisify(fs.readFile); - -/** - * Read all references for this service and add them to the array of references. - * - * Note that nothing is determined from the filename - the reference itself contains - * enough data to determine its eventual URL. - */ -const loadReferences = async (serviceDirectory, references) => { - const filenames = [ - path.join(serviceDirectory, 'references', 'api.json'), - // allow either events.json or (more correctly) exchanges.json - path.join(serviceDirectory, 'references', 'events.json'), - path.join(serviceDirectory, 'references', 'exchanges.json'), - ]; - - await Promise.all(filenames.map(async filename => { - let content; - try { - content = await readFile(filename); - } catch (e) { - if (e.code !== 'ENOENT') { - throw e; - } - return; // ignore ENOENT - } - - references.push(JSON.parse(content)); - })); -}; - -/** - * Read all schemas for this service and add them to the array of schemas - * - * Note that the schema filenames are ignored - the schema's $id is enough to determine - * its eventual URL. - */ -const loadSchemas = async (serviceDirectory, schemas) => { - const schemasDir = path.join(serviceDirectory, 'schemas'); - if (!await exists(schemasDir)) { - return; - } - - const queue = [schemasDir]; - while (queue.length) { - const filename = queue.shift(); - const st = await lstat(filename); - if (st.isDirectory()) { - for (let dentry of await readdir(filename)) { - queue.push(path.join(filename, dentry)); - } - } else { - schemas.push(JSON.parse(await readFile(filename))); - } - } -}; - -/** - * Load all schemas and references from `input`. - */ -const load = async ({input}) => { - const references = []; - const schemas = []; - - await Promise.all((await readdir(input)).map(async dentry => { - const filename = path.join(input, dentry); - if (!(await lstat(filename)).isDirectory()) { - throw new Error(`${filename} is not a directory`); - } - - // load the metadata and check the version - const metadata = JSON.parse(await readFile(path.join(filename, 'metadata.json'))); - if (metadata.version !== 1) { - throw new Error(`${dentry}: unrecognized metadata version`); - } - - await Promise.all([ - loadReferences(filename, references), - loadSchemas(filename, schemas), - ]); - })); - - return {references, schemas}; -}; - -exports.load = load; diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 9e443f0..0000000 --- a/src/main.js +++ /dev/null @@ -1,25 +0,0 @@ -const {load} = require('./load'); -const {update} = require('./update'); -const {store} = require('./store'); - -const build = async (input, output, rootUrl) => { - const {references, schemas} = await load({input}); - await update({references, schemas, rootUrl}); - await store({references, schemas, output, rootUrl}); -}; - -if (!module.parent) { - if (!process.env.TASKCLUSTER_ROOT_URL) { - console.error('TASKCLUSTER_ROOT_URL is not set'); - process.exit(1); - } - - const input = process.argv[2]; - const output = process.argv[3]; - if (!input || !output) { - console.error('usage: node src/main.js '); - process.exit(1); - } - - build(input, output, process.env.TASKCLUSTER_ROOT_URL); -} diff --git a/src/references.js b/src/references.js new file mode 100644 index 0000000..abce494 --- /dev/null +++ b/src/references.js @@ -0,0 +1,385 @@ +const builtServices = require('./built-services'); +const {makeSerializable, fromSerializable} = require('./serializable'); +const {writeUriStructured, readUriStructured} = require('./uri-structured'); +const {getCommonSchemas} = require('./common-schemas'); +const Ajv = require('ajv'); +const merge = require('lodash/merge'); +const {URL} = require('url'); +const regexEscape = require('regex-escape'); +const libUrls = require('taskcluster-lib-urls'); + +/** + * Representation of a set of references. This is considered immutable after + * construction. + * + * The public properties of this + * * `rootUrl` - the rootUrl (for absolute) or undefined (for abstract) + * * `schemas` - an array of schemas of the form {filename, content} + * * `references` - an array of references of the form {filename, content} + * + * The internal data structure is deliberately simple so as not to assume too + * much validity outside of the validate() function. The stored filenames are + * used only for error messages from validation, etc. + */ +class References { + constructor({rootUrl, schemas, references}) { + this.rootUrl = rootUrl; + this.schemas = schemas; + this.references = references; + + // avoid validating more than once + this._validated = false; + + // caches to make operations faster, but which assume validity + this._schemasById = null; + } + + /** + * Create a new representation from a "Built Services" formatted + * directory. + * + * The data in the directory will be amended with the "common" schemas and + * meta-schemas. + */ + static fromBuiltServices({directory}) { + let {references, schemas} = builtServices.load({directory}); + schemas = schemas.concat(getCommonSchemas()); + return new References({ + rootUrl: undefined, + references, + schemas}); + } + + /** + * Create a new representation from a URI-structured directory. + * No new "common" schemas or meta-schemas will be added. + * + * If the data is absolute, provide the rootUrl; for abstract data, pass + * rootUrl: undefined. + */ + static fromUriStructured({directory, rootUrl}) { + return References.fromSerializable({ + serializable: readUriStructured({directory}), + rootUrl, + }); + } + + /** + * Create a new representation from a serializable format. + * No new "common" schemas or meta-schemas will be added. + * + * If the data is absolute, provide the rootUrl; for abstract data, pass + * rootUrl: undefined. + */ + static fromSerializable({serializable, rootUrl}) { + return new References({ + rootUrl, + ...fromSerializable({serializable}), + }); + } + + /** + * Validate that all components of this instance are self-consistent. Throws + * an exception for any discovered issues. This can optionally be done with + * respect to a rootUrl, but this is only useful for performance reasons. + */ + validate() { + if (this._validated) { + return; + } + + // to validate an abstract References, temporarily qualify it with a + // rootUrl that is unlikely to appear in the content otherwise + // (specifically, not the testRootUrl) + if (!this.rootUrl) { + const absolute = this.asAbsolute('https://validate-root.example.com'); + absolute.validate(); + this._validated = true; + return; + } + + const problems = []; + + // first check for some basic structural issues that will cause Ajv to + // be sad.. + + let schemaPattern; // capture group 1 == prefix up to and including service name) + if (this.rootUrl === 'https://taskcluster.net') { + schemaPattern = new RegExp('(^https:\/\/schemas\.taskcluster\.net\/[^\/]*\/).*\.json#'); + } else { + schemaPattern = new RegExp(`(^${regexEscape(this.rootUrl)}\/schemas\/[^\/]*\/).*\.json#`); + } + + for (let {filename, content} of this.schemas) { + if (!content.$id) { + problems.push(`schema ${filename} has no $id`); + } else if (!schemaPattern.test(content.$id)) { + problems.push(`schema ${filename} has an invalid $id '${content.$id}' ` + + '(expected \'/schemas//something>.json#\''); + } + + if (!content.$schema) { + problems.push(`schema ${filename} has no $schema`); + } else if (!content.$schema.startsWith('http://json-schema.org') && !this._getSchema(content.$schema)) { + problems.push(`schema ${filename} has invalid $schema (must be defined here or be on at json-schema.org)`); + } + } + + const metadataMetaschema = libUrls.schema(this.rootUrl, 'common', 'metadata-metaschema.json#'); + for (let {filename, content} of this.references) { + if (!content.$schema) { + problems.push(`reference ${filename} has no $schema`); + } else if (!this._getSchema(content.$schema)) { + problems.push(`reference ${filename} has invalid $schema (must be defined here)`); + } else { + const schema = this._getSchema(content.$schema); + if (schema.$schema !== metadataMetaschema) { + problems.push(`reference ${filename} has schema '${content.$schema}' which does not have ` + + 'the metadata metaschema'); + } + } + } + + // if that was OK, check references in all schemas + + if (!problems.length) { + for (let {filename, content} of this.schemas) { + const idUrl = new URL(content.$id, this.rootUrl); + + const match = schemaPattern.exec(content.$id); + const refRoot = new URL(match[1], this.rootUrl); + + const refOk = ref => { + if (ref.startsWith('#')) { + return true; // URL doesn't like fragment-only relative URLs, but they are OK.. + } + + const refUrl = new URL(ref, idUrl).toString(); + return refUrl.startsWith(refRoot) || refUrl.startsWith('http://json-schema.org/'); + }; + + const checkRefs = (value, path) => { + if (Array.isArray(value)) { + value.forEach((v, i) => checkRefs(v, `${path}[${i}]`)); + } else if (typeof value === 'object') { + if (value.$ref && Object.keys(value).length === 1) { + if (!refOk(value.$ref)) { + problems.push(`schema ${filename} $ref at ${path} is not allowed`); + } + } else { + for (const [k, v] of Object.entries(value)) { + checkRefs(v, `${path}.${k}`); + } + } + } + }; + if (!content.$id.endsWith('metadata-metaschema.json#')) { + checkRefs(content, 'schema'); + } + } + } + + // if that was OK, validate everything against its declared schema. This is the part + // that requires a real rootUrl, since $schema cannot be a relative URL + + if (!problems.length) { + const ajv = this._makeAjv({schemas: this.schemas}); + + for (let {filename, content} of this.schemas) { + try { + ajv.validateSchema(content); + } catch (err) { + problems.push(err.toString()); + continue; + } + if (ajv.errors) { + ajv + .errorsText(ajv.errors, {separator: '%%/%%', dataVar: 'schema'}) + .split('%%/%%') + .forEach(err => problems.push(`${filename}: ${err}`)); + } + } + + for (let {filename, content} of this.references) { + try { + ajv.validate(content.$schema, content); + } catch (err) { + problems.push(err.toString()); + continue; + } + if (ajv.errors) { + ajv + .errorsText(ajv.errors, {separator: '%%/%%', dataVar: 'reference'}) + .split('%%/%%') + .forEach(err => problems.push(`${filename}: ${err}`)); + } + } + } + + if (problems.length) { + throw new ValidationProblems(problems); + } + + this._validated = true; + } + + /** + * Write out a URI-structured form of this instance. + */ + writeUriStructured({directory}) { + writeUriStructured({ + directory, + serializable: this.makeSerializable(), + }); + } + + /** + * Return a serializable form of this instance. + */ + makeSerializable() { + this.validate(); + return makeSerializable({references: this}); + } + + /** + * Create an Ajv instance with all schemas and metaschemas installed, + * using the given rootUrl (required because abstract schemas are not + * valid). + */ + makeAjv() { + if (!this.rootUrl) { + throw new Error('makeAjv is only valid on absolute References'); + } + this.validate(); + return this._makeAjv(); + } + + _makeAjv({schemas}) { + // validation requires an Ajv instance, so set that up without validating + if (!this._ajv) { + const ajv = new Ajv({ + format: 'full', + verbose: true, + allErrors: true, + validateSchema: false, + }); + + ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); + + // identify metaschemas, so we can all addMetaSchema for them + const metaSchemas = new Set(schemas.map(({content}) => content.$schema)); + for (let {content} of schemas) { + // try to be resilient to bad schemas, as validation should be able to give + // better error messages about schema problems. + if (!content.$id) { + return; + } + + if (metaSchemas.has(content.$id)) { + ajv.addMetaSchema(content); + } else { + ajv.addSchema(content); + } + } + + this._ajv = ajv; + } + return this._ajv; + } + + /** + * Get a particular schmea by its $id. + */ + getSchema($id) { + this.validate(); + return this._getSchema($id); + } + + _getSchema($id) { + if (!this._schemasById) { + this._schemasById = this.schemas.reduce( + (schemas, {content}) => schemas.set(content.$id, content), new Map()); + } + + return this._schemasById.get($id); + } + + /** + * Return a new instance that is not relative to any rootUrl. + */ + asAbstract() { + if (!this.rootUrl) { + return new References(this); + } + + let withoutRootUrl; + if (this.rootUrl === 'https://taskcluster.net') { + withoutRootUrl = uri => uri.replace(/^https:\/\/([^.]*)\.taskcluster\.net\//, '/$1/'); + } else { + const rootUrlPrefix = new RegExp(`^${regexEscape(this.rootUrl)}`); + withoutRootUrl = uri => uri.replace(rootUrlPrefix, ''); + } + + return new References({ + rootUrl: undefined, + ...this._withRewrittenUrls(withoutRootUrl), + }); + } + + /** + * Return a new instance that is relative to the given rootUrl. + */ + asAbsolute(rootUrl) { + if (this.rootUrl) { + return this.asAbstract().asAbsolute(rootUrl); + } + + let withRootUrl; + if (rootUrl === 'https://taskcluster.net') { + const rootUrlPrefix = /^\/(references|schemas)\//; + withRootUrl = uri => uri.replace(rootUrlPrefix, 'https://$1.taskcluster.net/'); + } else { + withRootUrl = uri => uri[0] === '/' ? rootUrl + uri : uri; + } + + return new References({ + rootUrl, + ...this._withRewrittenUrls(withRootUrl), + }); + } + + _withRewrittenUrls(rewrite) { + return { + references: + this.references.map(({content, filename}) => ({ + content: { + ...content, + $schema: content.$schema && rewrite(content.$schema), + }, + filename, + })), + schemas: + this.schemas.map(({content, filename}) => ({ + content: { + ...content, + $schema: content.$schema && rewrite(content.$schema), + $id: content.$id && rewrite(content.$id), + }, + filename, + })), + }; + } +} + +/** + * An error indicating validation failed. This has a ;-separated + * message, or the problems themselves are in the array err.problems. + */ +class ValidationProblems extends Error { + constructor(problems) { + super(problems.join('; ')); + this.problems = problems; + } +} + +exports.References = References; diff --git a/src/serializable.js b/src/serializable.js new file mode 100644 index 0000000..fdd202a --- /dev/null +++ b/src/serializable.js @@ -0,0 +1,75 @@ +const libUrls = require('taskcluster-lib-urls'); +const regexEscape = require('regex-escape'); + +/** + * Make a "serializable" data structure from the given references + */ +const makeSerializable = ({references}) => { + const urls = libUrls.withRootUrl(references.rootUrl || ''); + const referenceFilename = content => { + const serviceName = content.serviceName; + const apiVersion = content.apiVersion || 'v1'; + const kind = references.getSchema(content.$schema).metadata.name; + return `references/${serviceName}/${apiVersion || 'v1'}/${kind}.json`; + }; + + const namedReferences = references.references.map(({filename, content}) => ({ + content, + filename: referenceFilename(content), + })); + + let urlPattern; + if (references.rootUrl === 'https://taskcluster.net') { + urlPattern = /^https:\/\/schemas\.taskcluster\.net\/(.*)#/; + } else if (references.rootUrl) { + urlPattern = new RegExp(`^${regexEscape(references.rootUrl)}/schemas\/(.*)#`); + } else { + urlPattern = /^\/schemas\/(.*)#/; + } + const schemaFilename = content => content.$id.replace(urlPattern, 'schemas/$1'); + + const namedSchemas = references.schemas.map(({filename, content}) => ({ + content, + filename: schemaFilename(content), + })); + + const manifest = { + $schema: urls.schema('common', 'manifest-v3.json#'), + references: namedReferences.map(({filename}) => { + if (references.rootUrl === 'https://taskcluster.net') { + return filename.replace(/^references/, 'https://references.taskcluster.net'); + } else if (references.rootUrl) { + return `${references.rootUrl}/${filename}`; + } else { + return `/${filename}`; + } + }).sort(), + }; + + return [{ + filename: 'references/manifest.json', + content: manifest, + }].concat(namedSchemas).concat(namedReferences); +}; + +const fromSerializable = ({serializable}) => { + const references = []; + const schemas = []; + + serializable.forEach(({filename, content}) => { + if (filename.startsWith('schemas/')) { + schemas.push({filename, content}); + } else if (filename === 'references/manifest.json') { + // ignore an existing manifest + } else if (filename.startsWith('references/')) { + references.push({filename, content}); + } else { + throw new Error(`unexpected file: ${filename}`); + } + }); + + return {references, schemas}; +}; + +exports.makeSerializable = makeSerializable; +exports.fromSerializable = fromSerializable; diff --git a/src/update.js b/src/update.js deleted file mode 100644 index 10351ff..0000000 --- a/src/update.js +++ /dev/null @@ -1,82 +0,0 @@ -const _ = require('lodash'); -const url = require('url'); -const SchemaSet = require('taskcluster-lib-validate'); -const libUrls = require('taskcluster-lib-urls'); - -/** - * Update the given references and schemas (in-place): - * - * * add common schemas from this repository's schemas/ directory - * * make schemas' $id's relative to rootUrl (and use $id rather than id) - * * set references' serviceName and version and remove deprecated fields - */ -exports.update = async ({references, schemas, rootUrl}) => { - await addCommonSchemas(schemas); - updateReferences(references, rootUrl); - updateSchemas(schemas, rootUrl); -}; - -/** - * Calculate the reference's serviceName and guess at version (v1), then remove - * deprecated fields name and baseUrl. - */ -const updateReferences = (references, rootUrl) => { - references.forEach(reference => { - let serviceName = reference.serviceName; - if (!serviceName) { - if (reference.name) { - serviceName = reference.name; - } else if (reference.baseUrl) { - serviceName = reference.baseUrl.split('//')[1].split('.')[0]; - } else if (reference.exchangePrefix) { - serviceName = reference.exchangePrefix.split('/')[1].replace('taskcluster-', ''); - } - } - reference.serviceName = serviceName; - - if (!reference.version) { - reference.version = 'v1'; - } - - delete reference.name; - delete reference.baseUrl; - - // Someday, when we are not munging the reference above, this can allow - // multiple versions of the schema. For now, assign the only schema. - if (reference.exchangePrefix) { - reference.$schema = libUrls.schema(rootUrl, 'common', 'exchanges-reference-v0.json#'); - } else { - reference.$schema = libUrls.schema(rootUrl, 'common', 'api-reference-v0.json#'); - } - }); -}; -exports.updateReferences = updateReferences; - -/** - * Load common schemas stored in this (taskcluster-references) repository. - */ -const addCommonSchemas = async (schemas) => { - const schemaset = new SchemaSet({serviceName: 'common'}); - _.values(schemaset.abstractSchemas()).forEach(schema => { - schemas.push(schema); - }); -}; -exports.addCommonSchemas = addCommonSchemas; - -/** - * Update the $id property in all schemas - */ -const updateSchemas = (schemas, rootUrl) => { - schemas.forEach(schema => { - const $id = url.parse(schema.$id || schema.id); - - // compatibility with old, non-r13y schemas (this can be removed when none remain) - if ($id.hostname === 'schemas.taskcluster.net') { - schema.$id = url.resolve(rootUrl, '/schemas' + $id.pathname) + '#'; - } else { - schema.$id = url.resolve(rootUrl, $id.pathname) + '#'; - } - delete schema.id; - }); -}; -exports.updateSchemas = updateSchemas; diff --git a/src/uri-structured.js b/src/uri-structured.js new file mode 100644 index 0000000..fd2ccdf --- /dev/null +++ b/src/uri-structured.js @@ -0,0 +1,45 @@ +const mkdirp = require('mkdirp'); +const rimraf = require('rimraf'); +const path = require('path'); +const fs = require('fs'); + +const writeUriStructured = ({directory, serializable}) => { + rimraf.sync(directory); + + const dirs = new Set(); + for (let {filename, content} of serializable) { + const pathname = path.join(directory, filename); + const dirname = path.dirname(pathname); + if (!dirs.has(dirname)) { + mkdirp.sync(dirname); + dirs.add(dirname); + } + fs.writeFileSync(pathname, JSON.stringify(content, null, 2)); + } +}; + +const readUriStructured = ({directory}) => { + const files = []; + + const queue = ['.']; + while (queue.length) { + const filename = queue.shift(); + const fqfilename = path.join(directory, filename); + const st = fs.lstatSync(fqfilename); + if (st.isDirectory()) { + for (let dentry of fs.readdirSync(fqfilename)) { + queue.push(path.join(filename, dentry)); + } + } else { + files.push({ + filename, + content: JSON.parse(fs.readFileSync(fqfilename)), + }); + } + } + + return files; +}; + +exports.writeUriStructured = writeUriStructured; +exports.readUriStructured = readUriStructured; diff --git a/test/built-services_test.js b/test/built-services_test.js new file mode 100644 index 0000000..c33a515 --- /dev/null +++ b/test/built-services_test.js @@ -0,0 +1,93 @@ +const fs = require('fs'); +const assert = require('assert'); +const {load} = require('../src/built-services'); +const mockFs = require('mock-fs'); +const References = require('..'); + +suite('built-services_test.js', function() { + teardown(function() { + mockFs.restore(); + }); + + test('fails on files in the input dir', function() { + mockFs({'/test/input/some.data': 'junk'}); + assert.throws( + () => load({directory: '/test/input'}), + /some.data is not a directory/); + }); + + test('fails on dirs without metadata.json', function() { + mockFs({'/test/input/svc': {}}); + assert.throws( + () => load({directory: '/test/input'}), + /no such file or directory .*metadata.json/); + }); + + test('fails on metadata.json with unknown version', function() { + mockFs({'/test/input/svc/metadata.json': '{"version": 17}'}); + assert.throws( + () => load({directory: '/test/input'}), + /unrecognized metadata version/); + }); + + const setupFs = () => { + mockFs({ + '/test/input/svc1/metadata.json': '{"version": 1}', + '/test/input/svc1/references/api.json': '{"api": 1, "$schema": "/sch"}', + '/test/input/svc1/references/events.json': '{"exchanges": 1, "$schema": "/sch"}', + '/test/input/svc2/metadata.json': '{"version": 1, "$schema": "/sch"}', + '/test/input/svc2/references/api.json': '{"api": 2, "$schema": "/sch"}', + '/test/input/svc3/metadata.json': '{"version": 1}', + '/test/input/svc3/references/exchanges.json': '{"exchanges": 3, "$schema": "/sch"}', + }); + }; + + test('reads references', function() { + setupFs(); + const {references, schemas} = load({directory: '/test/input'}); + assert.deepEqual(references.map(ref => JSON.stringify(ref.content)).sort(), [ + '{"api":1,"$schema":"/sch"}', + '{"api":2,"$schema":"/sch"}', + '{"exchanges":1,"$schema":"/sch"}', + '{"exchanges":3,"$schema":"/sch"}', + ]); + assert.deepEqual(schemas, []); + }); + + test('References.fromBuiltServices reads references and adds common', function() { + setupFs(); + const references = References.fromBuiltServices({directory: '/test/input'}); + assert.deepEqual(references.references.map(ref => JSON.stringify(ref.content)).sort(), [ + '{"api":1,"$schema":"/sch"}', + '{"api":2,"$schema":"/sch"}', + '{"exchanges":1,"$schema":"/sch"}', + '{"exchanges":3,"$schema":"/sch"}', + ]); + // check for one of the common schemas + const ids = references.schemas.map(({content}) => content.$id).sort(); + assert(ids.some(id => id === '/schemas/common/manifest-v3.json#')); + }); + + test('reads schemas at all nesting levels', function() { + mockFs({ + '/test/input/svc1/metadata.json': '{"version": 1}', + '/test/input/svc1/schemas': { + 'root.json': '"root"', + v1: { + 'versioned.json': '"versioned"', + }, + v2: { + deep: { + dir: { + 'structure.json': '"deeper"', + }, + }, + }, + }, + }); + const {references, schemas} = load({directory: '/test/input'}); + assert.deepEqual(references, []); + assert.deepEqual(schemas.map(sch => JSON.stringify(sch.content)).sort(), + ['"deeper"', '"root"', '"versioned"']); + }); +}); diff --git a/test/common-schemas_test.js b/test/common-schemas_test.js new file mode 100644 index 0000000..1185634 --- /dev/null +++ b/test/common-schemas_test.js @@ -0,0 +1,12 @@ +const assert = require('assert'); +const {getCommonSchemas} = require('../src/common-schemas'); + +suite('common-schemas_test.js', function() { + test('loads common schemas', function() { + const schemas = getCommonSchemas(); + assert(schemas.some( + ({content, filename}) => content.$id === '/schemas/common/api-reference-v0.json#')); + assert(schemas.some( + ({content, filename}) => filename === 'schemas/metadata-metaschema.yml')); + }); +}); diff --git a/test/helper.js b/test/helper.js deleted file mode 100644 index 30de7cd..0000000 --- a/test/helper.js +++ /dev/null @@ -1,23 +0,0 @@ -const assert = require('assert'); - -/** - * A poor man's "assert.rejects", since assert.rejects was only introduced - * in Node 10 - */ -exports.assert_rejects = async (promise, messageRegexp) => { - let err; - - try { - await promise; - } catch (e) { - err = e; - } - if (!err) { - throw new assert.AssertionError({ - message: 'Did not get expected error', - }); - } - if (!err.toString().match(messageRegexp)) { - throw err; - } -}; diff --git a/test/load_test.js b/test/load_test.js deleted file mode 100644 index 25f909e..0000000 --- a/test/load_test.js +++ /dev/null @@ -1,71 +0,0 @@ -const fs = require('fs'); -const assert = require('assert'); -const {load} = require('../src/load'); -const mockFs = require('mock-fs'); -const {assert_rejects} = require('./helper'); - -suite('loading input', function() { - teardown(function() { - mockFs.restore(); - }); - - test('fails on files in the input dir', async function() { - mockFs({'/test/input/some.data': 'junk'}); - await assert_rejects( - load({input: '/test/input'}), - /some.data is not a directory/); - }); - - test('fails on dirs without metadata.json', async function() { - mockFs({'/test/input/svc': {}}); - await assert_rejects( - load({input: '/test/input'}), - /no such file or directory .*metadata.json/); - }); - - test('fails on metadata.json with unknown version', async function() { - mockFs({'/test/input/svc/metadata.json': '{"version": 17}'}); - await assert_rejects( - load({input: '/test/input'}), - /unrecognized metadata version/); - }); - - test('reads references', async function() { - mockFs({ - '/test/input/svc1/metadata.json': '{"version": 1}', - '/test/input/svc1/references/api.json': '{"api": 1}', - '/test/input/svc1/references/events.json': '{"exchanges": 1}', - '/test/input/svc2/metadata.json': '{"version": 1}', - '/test/input/svc2/references/api.json': '{"api": 2}', - '/test/input/svc3/metadata.json': '{"version": 1}', - '/test/input/svc3/references/exchanges.json': '{"exchanges": 3}', - }); - const {references, schemas} = await load({input: '/test/input'}); - assert.deepEqual(references.map(JSON.stringify).sort(), - ['{"api":1}', '{"api":2}', '{"exchanges":1}', '{"exchanges":3}']); - assert.deepEqual(schemas, []); - }); - - test('reads schemas at all nesting levels', async function() { - mockFs({ - '/test/input/svc1/metadata.json': '{"version": 1}', - '/test/input/svc1/schemas': { - 'root.json': '"root"', - v1: { - 'versioned.json': '"versioned"', - }, - v2: { - deep: { - dir: { - 'structure.json': '"deeper"', - }, - }, - }, - }, - }); - const {references, schemas} = await load({input: '/test/input'}); - assert.deepEqual(references, []); - assert.deepEqual(schemas.map(JSON.stringify).sort(), - ['"deeper"', '"root"', '"versioned"']); - }); -}); diff --git a/test/references_test.js b/test/references_test.js new file mode 100644 index 0000000..b942d41 --- /dev/null +++ b/test/references_test.js @@ -0,0 +1,252 @@ +const assert = require('assert'); +const fs = require('fs'); +const {getCommonSchemas} = require('../src/common-schemas'); +const libUrls = require('taskcluster-lib-urls'); +const {makeSerializable} = require('../src/serializable'); +const mockFs = require('mock-fs'); +const merge = require('lodash/merge'); +const omit = require('lodash/omit'); +const References = require('..'); + +suite('references_test.js', function() { + const rootUrl = libUrls.testRootUrl(); + + teardown(function() { + mockFs.restore(); + }); + + const references = new References({ + schemas: getCommonSchemas(), + references: [], + }); + + test('getSchema', function() { + assert.equal( + references.getSchema('/schemas/common/manifest-v3.json#').$id, + '/schemas/common/manifest-v3.json#'); + }); + + test('makeSerializable', function() { + assert.deepEqual( + references.makeSerializable(), + makeSerializable({references})); + }); + + test('writes uri-structured', function() { + mockFs({}); + const references = new References({ + references: [], + schemas: getCommonSchemas().concat([{ + filename: 'foo.json', + content: { + $schema: '/schemas/common/metadata-metaschema.json#', + $id: '/schemas/test/sch.json#', + metadata: {name: 'api', version: 1}, + type: 'string', + }, + }]), + }); + + references.writeUriStructured({directory: '/refdata'}); + assert.deepEqual(JSON.parse(fs.readFileSync('/refdata/schemas/test/sch.json')), { + $id: '/schemas/test/sch.json#', + $schema: '/schemas/common/metadata-metaschema.json#', + metadata: {name: 'api', version: 1}, + type: 'string', + }); + }); + + suite('validate', function() { + class RefBuilder { + constructor() { + this.schemas = [...getCommonSchemas()]; + this.references = []; + } + + schema({omitPaths=[], filename='test-schema.yml', ...content}) { + this.schemas.push({ + filename, + content: omit(merge({ + $schema: 'http://json-schema.org/draft-06/schema#', + $id: '/schemas/common/test.json#', + }, content), omitPaths), + }); + return this; + } + + apiref({omitPaths=[], filename='test-api-ref.yml', ...content}) { + this.references.push({ + filename, + content: omit(merge({ + $schema: '/schemas/common/api-reference-v0.json#', + version: 0, + apiVersion: 'v2', + serviceName: 'test', + baseUrl: 'http://test.localhost', + title: 'Test Service', + description: 'Test Service', + entries: [], + }, content), omitPaths), + }); + return this; + } + + end() { + return new References(this); + } + } + + const assertProblems = (references, expected) => { + try { + references.validate(); + } catch (e) { + if (!expected.length || !e.problems) { + throw e; + } + assert.deepEqual(e.problems.sort(), expected.sort()); + return; + } + if (expected.length) { + throw new Error('Expected problems not identified'); + } + }; + + test('empty references pass', function() { + const references = new RefBuilder().end(); + assertProblems(references, []); + }); + + test('schema with no $id fails', function() { + const references = new RefBuilder() + .schema({omitPaths: ['$id']}) + .end(); + assertProblems(references, ['schema test-schema.yml has no $id']); + }); + + test('schema with invalid $id fails', function() { + const references = new RefBuilder() + .schema({$id: '/schemas/foo.yml'}) + .end(); + assertProblems(references, [ + 'schema test-schema.yml has an invalid $id \'https://validate-root.example.com/schemas/foo.yml\' ' + + '(expected \'/schemas//something>.json#\'', + ]); + }); + + test('schema with invalid absolute $ref fails', function() { + const references = new RefBuilder() + .schema({ + type: 'object', + properties: { + foo: {$ref: 'https://example.com/schema.json#'}, + }, + }) + .end(); + assertProblems(references, [ + 'schema test-schema.yml $ref at schema.properties.foo is not allowed', + ]); + }); + + test('schema with invalid relative $ref fails', function() { + const references = new RefBuilder() + .schema({ + type: 'object', + properties: { + foo: {$ref: '../uncommon/foo.json#'}, + }, + }) + .end(); + assertProblems(references, [ + 'schema test-schema.yml $ref at schema.properties.foo is not allowed', + ]); + }); + + test('schema with no metaschema fails', function() { + const references = new RefBuilder() + .schema({omitPaths: ['$schema']}) + .end(); + assertProblems(references, ['schema test-schema.yml has no $schema']); + }); + + test('schema with custom metaschema passes', function() { + const references = new RefBuilder() + .schema({ + $schema: '/schemas/common/metadata-metaschema.json#', + metadata: {name: 'api', version: 1}, + }) + .end(); + assertProblems(references, []); + }); + + test('invalid schema fails', function() { + const references = new RefBuilder() + .schema({ + type: 'object', + properties: { + abc: ['a'], + }, + }) + .end(); + assertProblems(references, [ + 'test-schema.yml: schema.properties[\'abc\'] should be object,boolean', + ]); + }); + + test('invalid schema with custom metaschema passes', function() { + const references = new RefBuilder() + .schema({ + $schema: '/schemas/common/metadata-metaschema.json#', + metadata: {version: 1}, + }) + .end(); + assertProblems(references, [ + 'test-schema.yml: schema.metadata should have required property \'name\'', + ]); + }); + + test('schema with undefined metaschema fails', function() { + const references = new RefBuilder() + .schema({$schema: '/schemas/nosuch.json#'}) + .end(); + assertProblems(references, [ + 'schema test-schema.yml has invalid $schema (must be defined here or be on at json-schema.org)', + ]); + }); + + test('reference with no $schema fails', function() { + const references = new RefBuilder() + .apiref({omitPaths: ['$schema']}) + .end(); + assertProblems(references, ['reference test-api-ref.yml has no $schema']); + }); + + test('invalid reference fails', function() { + const references = new RefBuilder() + .apiref({entries: true}) + .end(); + assertProblems(references, [ + 'test-api-ref.yml: reference.entries should be array', + ]); + }); + + test('reference with undefined $schema fails', function() { + const references = new RefBuilder() + .apiref({$schema: '/schemas/nosuch.json#'}) + .end(); + assertProblems(references, [ + 'reference test-api-ref.yml has invalid $schema (must be defined here)', + ]); + }); + + test('reference with non-metadata metaschema fails', function() { + const references = new RefBuilder() + .apiref({$schema: '/schemas/common/metadata-metaschema.json#'}) + .end(); + assertProblems(references, [ + 'reference test-api-ref.yml has schema ' + + '\'https://validate-root.example.com/schemas/common/metadata-metaschema.json#\' ' + + 'which does not have the metadata metaschema', + ]); + }); + }); +}); diff --git a/test/serializable_test.js b/test/serializable_test.js new file mode 100644 index 0000000..a605627 --- /dev/null +++ b/test/serializable_test.js @@ -0,0 +1,160 @@ +const assert = require('assert'); +const References = require('..'); +const {makeSerializable, fromSerializable} = require('../src/serializable'); +const {getCommonSchemas} = require('../src/common-schemas'); +const libUrls = require('taskcluster-lib-urls'); + +suite('serializable_test.js', function() { + const rootUrl = libUrls.testRootUrl(); + const legacyRootUrl = 'https://taskcluster.net'; + + const assert_file = (serializable, filename, content) => { + for (let file of serializable) { + if (file.filename !== filename) { + continue; + } + if (typeof content === 'function') { + content(file.content); + } else { + assert.deepEqual(file.content, content); + } + return; + } + throw new Error(`filename ${filename} not found`); + }; + + const references = new References({ + schemas: getCommonSchemas(), + references: [{ + filename: 'test-ref.json', + content: { + $schema: '/schemas/common/api-reference-v0.json#', + version: 0, + title: 'test', + description: 'test', + baseUrl: 'https://foo', + serviceName: 'test', + apiVersion: 'v1', + entries: [], + }, + }, { + filename: 'test2-ref.json', + content: { + $schema: '/schemas/common/exchanges-reference-v0.json#', + serviceName: 'test2', + apiVersion: 'v2', + title: 'test', + description: 'test', + exchangePrefix: 'x', + entries: [], + }, + }], + }); + + const sortedBy = (array, prop) => { + return array.map(({content}) => content).sort((a, b) => { + if (a[prop] < b[prop]) { + return -1; + } else if (a[prop] > b[prop]) { + return 1; + } else { + return 0; + } + }); + }; + + test('generates an abstract manifest', function() { + const serializable = makeSerializable({references}); + assert_file(serializable, 'references/manifest.json', { + $schema: '/schemas/common/manifest-v3.json#', + references: [ + '/references/test/v1/api.json', + '/references/test2/v2/exchanges.json', + ], + }); + }); + + test('generates an absolute manifest', function() { + const serializable = makeSerializable({references: references.asAbsolute(rootUrl)}); + assert_file(serializable, 'references/manifest.json', { + $schema: rootUrl + '/schemas/common/manifest-v3.json#', + references: [ + rootUrl + '/references/test/v1/api.json', + rootUrl + '/references/test2/v2/exchanges.json', + ], + }); + }); + + test('generates an absolute manifest for legacy rootUrl', function() { + const serializable = makeSerializable({references: references.asAbsolute(legacyRootUrl)}); + assert_file(serializable, 'references/manifest.json', { + $schema: 'https://schemas.taskcluster.net/common/manifest-v3.json#', + references: [ + 'https://references.taskcluster.net/test/v1/api.json', + 'https://references.taskcluster.net/test2/v2/exchanges.json', + ], + }); + }); + + test('generates abstract schema filenames', function() { + const serializable = makeSerializable({references}); + assert_file(serializable, 'schemas/common/api-reference-v0.json', content => { + assert.equal(content.$schema, '/schemas/common/metadata-metaschema.json#'); + assert.equal(content.$id, '/schemas/common/api-reference-v0.json#'); + }); + }); + + test('generates absolute schema filenames', function() { + const serializable = makeSerializable({references: references.asAbsolute(rootUrl)}); + assert_file(serializable, 'schemas/common/api-reference-v0.json', content => { + assert.equal(content.$schema, rootUrl + '/schemas/common/metadata-metaschema.json#'); + assert.equal(content.$id, rootUrl + '/schemas/common/api-reference-v0.json#'); + }); + }); + + test('generates absolute schema filenames for legacy rootUrl', function() { + const serializable = makeSerializable({references: references.asAbsolute(legacyRootUrl)}); + assert_file(serializable, 'schemas/common/api-reference-v0.json', content => { + assert.equal(content.$schema, 'https://schemas.taskcluster.net/common/metadata-metaschema.json#'); + assert.equal(content.$id, 'https://schemas.taskcluster.net/common/api-reference-v0.json#'); + }); + }); + + test('generates an API reference filename', function() { + const serializable = makeSerializable({references}); + assert_file(serializable, 'references/test/v1/api.json', { + $schema: '/schemas/common/api-reference-v0.json#', + version: 0, + title: 'test', + description: 'test', + baseUrl: 'https://foo', + serviceName: 'test', + apiVersion: 'v1', + entries: [], + }); + }); + + test('generates an exchanges reference filename', function() { + const serializable = makeSerializable({references}); + assert_file(serializable, 'references/test2/v2/exchanges.json', { + $schema: '/schemas/common/exchanges-reference-v0.json#', + serviceName: 'test2', + apiVersion: 'v2', + title: 'test', + description: 'test', + exchangePrefix: 'x', + entries: [], + }); + }); + + test('References.fromSerializable', function() { + const unserialized = References.fromSerializable({ + serializable: [{ + filename: 'schemas/common/foo.json', + content: { + $id: '/schemas/common/foo.json#', + }, + }], + }); + }); +}); diff --git a/test/store_test.js b/test/store_test.js deleted file mode 100644 index 2155aec..0000000 --- a/test/store_test.js +++ /dev/null @@ -1,82 +0,0 @@ -const fs = require('fs'); -const assert = require('assert'); -const {store} = require('../src/store'); -const mockFs = require('mock-fs'); -const {assert_rejects} = require('./helper'); -const libUrls = require('taskcluster-lib-urls'); - -suite('storing output', function() { - const rootUrl = libUrls.testRootUrl(); - - teardown(function() { - mockFs.restore(); - }); - - test('deletes and re-creates output', async function() { - mockFs({ - '/test/output/junk': 'junk-data', - }); - assert(fs.existsSync('/test/output/junk')); - await store({references: [], schemas: [], output: '/test/output', rootUrl}); - assert(fs.existsSync('/test/output')); - assert(!fs.existsSync('/test/output/junk')); - }); - - test('writes references at the path given by serviceName/version, and detects exchanges.json', async function() { - mockFs({}); - await store({ - references: [ - {serviceName: 'fake', version: 'v1'}, - {serviceName: 'fake', exchangePrefix: 'exchanges/taskcluster-fake/v2', version: 'v2'}, - ], - schemas: [], - output: '/test/output', - rootUrl, - }); - assert(fs.existsSync('/test/output/references/fake/v1/api.json')); - assert(!fs.existsSync('/test/output/references/fake/v1/exchanges.json')); - assert(!fs.existsSync('/test/output/references/fake/v2/api.json')); - assert(fs.existsSync('/test/output/references/fake/v2/exchanges.json')); - - const ref1 = JSON.parse(fs.readFileSync('/test/output/references/fake/v1/api.json')); - assert.deepEqual(ref1, {serviceName: 'fake', version: 'v1'}); - const ref2 = JSON.parse(fs.readFileSync('/test/output/references/fake/v2/exchanges.json')); - assert.deepEqual(ref2, {serviceName: 'fake', exchangePrefix: 'exchanges/taskcluster-fake/v2', version: 'v2'}); - const manifest = JSON.parse(fs.readFileSync('/test/output/references/manifest.json')); - assert.deepEqual(manifest, { - $schema: 'https://tc-tests.localhost/schemas/common/manifest-v2.json#', - services: [ - { - serviceName: 'fake', - apis: [ - {version: 'v1', reference: '/references/fake/v1/api.json'}, - ], - pulse: [ - {version: 'v2', reference: '/references/fake/v2/exchanges.json'}, - ], - }, - ], - }); - }); - - test('writes schemas at the path given by their $id (without #)', async function() { - mockFs({}); - await store({ - references: [], - schemas: [ - {$id: 'https://tc-tests.localhost/schemas/fake/v1/foo.json#'}, - {$id: 'https://tc-tests.localhost/schemas/fake/v2/bar.json#'}, - ], - output: '/test/output', - rootUrl, - }); - - assert(fs.existsSync('/test/output/schemas/fake/v1/foo.json')); - assert(fs.existsSync('/test/output/schemas/fake/v2/bar.json')); - - const sch1 = JSON.parse(fs.readFileSync('/test/output/schemas/fake/v1/foo.json')); - assert.deepEqual(sch1, {$id: 'https://tc-tests.localhost/schemas/fake/v1/foo.json#'}); - const sch2 = JSON.parse(fs.readFileSync('/test/output/schemas/fake/v2/bar.json')); - assert.deepEqual(sch2, {$id: 'https://tc-tests.localhost/schemas/fake/v2/bar.json#'}); - }); -}); diff --git a/test/update_test.js b/test/update_test.js deleted file mode 100644 index c585525..0000000 --- a/test/update_test.js +++ /dev/null @@ -1,79 +0,0 @@ -const fs = require('fs'); -const assert = require('assert'); -const {update, updateReferences, addCommonSchemas, updateSchemas} = require('../src/update'); -const libUrls = require('taskcluster-lib-urls'); - -suite('updating', function() { - const refTest = (description, input, output) => { - test(description, function() { - const data = {references: [input], schemas: []}; - updateReferences([input], libUrls.testRootUrl()); - assert.deepEqual(input, output); - }); - }; - - const apiSchema = 'https://tc-tests.localhost/schemas/common/api-reference-v0.json#'; - const exchangeSchema = 'https://tc-tests.localhost/schemas/common/exchanges-reference-v0.json#'; - - refTest('updates a modern reference without change', - {serviceName: 'fake', version: 'v1'}, - {serviceName: 'fake', version: 'v1', $schema: apiSchema}); - - refTest('updates a reference with .name', - {name: 'fake', version: 'v1'}, - {serviceName: 'fake', version: 'v1', $schema: apiSchema}); - - refTest('updates a reference with .baseUrl', - {baseUrl: 'https://fake.taskcluster.net/v1', version: 'v1'}, - {serviceName: 'fake', version: 'v1', $schema: apiSchema}); - - refTest('updates a reference with .exchangePrefix (and does not delete it)', { - exchangePrefix: 'exchange/taskcluster-fake/v1', - version: 'v1', - }, { - exchangePrefix: 'exchange/taskcluster-fake/v1', - serviceName: 'fake', - version: 'v1', - $schema: exchangeSchema, - }); - - refTest('guesses at a missing version', - {baseUrl: 'https://fake.taskcluster.net/v1'}, - {serviceName: 'fake', version: 'v1', $schema: apiSchema}); - - const schemaTest = (description, input, output) => { - test(description, async function() { - updateSchemas([input], libUrls.testRootUrl()); - assert.deepEqual(input, output); - }); - }; - - schemaTest('relativizes a schema with $id', - {$id: 'taskcluster:/schemas/fake/v1/fake-data.json#'}, - {$id: libUrls.schema(libUrls.testRootUrl(), 'fake', 'v1/fake-data.json#')}); - - schemaTest('relativizes rootUrl=https://taskcluster.net schemas correctly', - {$id: 'https://schemas.taskcluster.net/fake/v1/fake-data.json#'}, - {$id: libUrls.schema(libUrls.testRootUrl(), 'fake', 'v1/fake-data.json#')}); - - schemaTest('adds # to $id', - {$id: 'taskcluster:/schemas/fake/v1/fake-data.json'}, - {$id: libUrls.schema(libUrls.testRootUrl(), 'fake', 'v1/fake-data.json#')}); - - schemaTest('moves id to $id', - {id: 'taskcluster:/schemas/fake/v1/fake-data.json#'}, - {$id: libUrls.schema(libUrls.testRootUrl(), 'fake', 'v1/fake-data.json#')}); - - test('adds common schemas', async function() { - const schemas = []; - await addCommonSchemas(schemas); - const ids = schemas.map(sch => sch.$id).sort(); - assert.deepEqual(ids, [ - 'taskcluster:/schemas/common/action-schema-v1.json#', - 'taskcluster:/schemas/common/api-reference-v0.json#', - 'taskcluster:/schemas/common/exchanges-reference-v0.json#', - 'taskcluster:/schemas/common/manifest-v2.json#', - ]); - }); -}); - diff --git a/test/uri-structured_test.js b/test/uri-structured_test.js new file mode 100644 index 0000000..82aaf22 --- /dev/null +++ b/test/uri-structured_test.js @@ -0,0 +1,71 @@ +const fs = require('fs'); +const assert = require('assert'); +const References = require('..'); +const {readUriStructured, writeUriStructured} = require('../src/uri-structured'); +const mockFs = require('mock-fs'); + +suite('uri-structured_test.js', function() { + teardown(function() { + mockFs.restore(); + }); + + test('writes files', function() { + mockFs({}); + + // write some data to check later that it's deleted + fs.mkdirSync('/refdata'); + fs.writeFileSync('/refdata/foo', 'bar'); + + writeUriStructured({ + directory: '/refdata', + serializable: [ + {filename: 'abc/def.json', content: {abc: 'def'}}, + {filename: 'abc.json', content: 'abc'}, + ], + }); + + assert(!fs.existsSync('/refdata/foo')); + assert.equal(fs.readFileSync('/refdata/abc/def.json'), '{\n "abc": "def"\n}'); + assert.equal(fs.readFileSync('/refdata/abc.json'), '"abc"'); + }); + + test('reads files', function() { + mockFs({ + '/data/schemas/common/foo.json': '{"foo": "true"}', + '/data/references/something/bar.json': '{"bar": "true"}', + }); + const files = readUriStructured({directory: '/data'}); + assert.deepEqual(files.sort(), [{ + filename: 'references/something/bar.json', + content: {bar: 'true'}, + }, { + filename: 'schemas/common/foo.json', + content: {foo: 'true'}, + }]); + }); + + test('fromUriStructured', function() { + mockFs({ + '/data/schemas/common/foo.json': + '{"foo": "true", "$id": "/schemas/common/foo.json", "$schema": "http://json-schema.org/draft-06/schema#"}', + '/data/references/something/bar.json': + '{"bar": "true", "$schema": "/schemas/common/foo.json#"}', + }); + const references = References.fromUriStructured({directory: '/data'}); + assert.deepEqual(references.references, [{ + filename: 'references/something/bar.json', + content: { + $schema: '/schemas/common/foo.json#', + bar: 'true', + }, + }]); + assert.deepEqual(references.schemas, [{ + filename: 'schemas/common/foo.json', + content: { + $id: '/schemas/common/foo.json', + $schema: 'http://json-schema.org/draft-06/schema#', + foo: 'true', + }, + }]); + }); +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..935d3dd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,993 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s= + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + integrity sha1-ReN/s56No/JbruP/U2niu18iAXo= + +acorn@^5.5.0: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + integrity sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I= + +ajv@^5.2.3, ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1" + integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +babel-code-frame@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" + integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-config-taskcluster@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-taskcluster/-/eslint-config-taskcluster-3.2.0.tgz#542db58e2cdb7b6f50c592d96028013b563619b6" + integrity sha512-BPApdNI6TM87yByuzVDffb60QvxmaqDeXn9vRazWxNaG1Twa2GNmZbb0piGpjT2p3pIacWXZOVo2jpMyIfi7Vg== + dependencies: + eslint "^4.10.0" + +eslint-scope@^3.7.1: + version "3.7.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535" + integrity sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== + +eslint@^4.10.0: + version "4.19.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" + integrity sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ== + dependencies: + ajv "^5.3.0" + babel-code-frame "^6.22.0" + chalk "^2.1.0" + concat-stream "^1.6.0" + cross-spawn "^5.1.0" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.4" + esquery "^1.0.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.0.1" + ignore "^3.3.3" + imurmurhash "^0.1.4" + inquirer "^3.0.6" + is-resolvable "^1.0.0" + js-yaml "^3.9.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.4" + minimatch "^3.0.2" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + regexpp "^1.0.1" + require-uncached "^1.0.3" + semver "^5.3.0" + strip-ansi "^4.0.0" + strip-json-comments "~2.0.1" + table "4.0.2" + text-table "~0.2.0" + +espree@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" + integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A== + dependencies: + acorn "^5.5.0" + acorn-jsx "^3.0.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + +external-editor@^2.0.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + integrity sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A== + dependencies: + chardet "^0.4.0" + iconv-lite "^0.4.17" + tmp "^0.0.33" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +flat-cache@^1.2.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" + integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg== + dependencies: + circular-json "^0.3.1" + graceful-fs "^4.1.2" + rimraf "~2.6.2" + write "^0.2.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.5, glob@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.0.1: + version "11.9.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.9.0.tgz#bde236808e987f290768a93d065060d78e6ab249" + integrity sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg== + +graceful-fs@^4.1.2: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + +iconv-lite@^0.4.17: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^3.3.3: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inquirer@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" + integrity sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ== + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.4" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx-lite "^4.0.8" + rx-lite-aggregates "^4.0.8" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= + +js-yaml@^3.12.0, js-yaml@^3.9.1: + version "3.12.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" + integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash@^4.17.10, lodash@^4.17.4, lodash@^4.3.0: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +lru-cache@^4.0.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" + integrity sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@0.5.1, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +mock-fs@^4.5.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.7.0.tgz#9f17e219cacb8094f4010e0a8c38589e2b33c299" + integrity sha512-WlQNtUlzMRpvLHf8dqeUmNqfdPjGY29KrJF50Ldb4AcL+vQeR8QH3wQcFMgrhTwb1gHjZn9xggho+84tBskLgA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +progress@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" + integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +regex-escape@^3.4.8: + version "3.4.8" + resolved "https://registry.yarnpkg.com/regex-escape/-/regex-escape-3.4.8.tgz#d819372b24fb2659174196ecbecfa69d8612f30d" + integrity sha512-DG0VFPTDwfl+XsuVaM/0RmGJvCpZNB9UG/limzbev50XQ44G4mbOG+0Eh5M7Al9JB68NbP7YeY1KhDzpnX7qSw== + +regexpp@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" + integrity sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw== + +require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +rimraf@^2.6.2, rimraf@~2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== + dependencies: + glob "^7.0.5" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +rx-lite-aggregates@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" + integrity sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74= + dependencies: + rx-lite "*" + +rx-lite@*, rx-lite@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" + integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.3.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== + dependencies: + is-fullwidth-code-point "^2.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== + dependencies: + has-flag "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" + integrity sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA== + dependencies: + ajv "^5.2.3" + ajv-keywords "^2.1.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + +taskcluster-lib-urls@^11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/taskcluster-lib-urls/-/taskcluster-lib-urls-11.0.1.tgz#ebdbffec41278c7bfa0075f42314e75f917632df" + integrity sha512-sapLD2um0MUuwtZh3zs6S7kfnb6AhxuYFvEUT+2VLYQ245hYZP2NcuI4MBtCIscR+CMesxPgxz7kEbotgnucZA== + +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= + dependencies: + mkdirp "^0.5.1" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=