From de364175eb85c6358cc524c0a897b736d46c67b0 Mon Sep 17 00:00:00 2001 From: Jean-Gab Date: Tue, 27 Apr 2021 09:33:00 -0400 Subject: [PATCH] Unique ids in blocks with nanoid (#1667) * feat: Add unique ids for each block * fix: Improve code based on code review * feat(block ids): Use nanoid library for block id generation * Remove unused files * Add tests * Fix lint & test * fix: Remove unnecessary id generation, use nanoid(10) to shorten the id, add changelog and some documentation Also improved some documentation along the lines and fixed linting * Update copy-paste.spec.ts * fix id generation, add api method * Update blocks.spec.ts * update tests Co-authored-by: cobb Co-authored-by: George Berezhnoy Co-authored-by: Georgy Berezhnoy Co-authored-by: Peter Savchenko --- docs/CHANGELOG.md | 3 + example/example-dev.html | 14 +++ package.json | 5 +- src/components/block/api.ts | 8 ++ src/components/block/index.ts | 17 +++- src/components/modules/api/blocks.ts | 18 ++++ src/components/modules/blockManager.ts | 30 ++++-- src/components/modules/paste.ts | 2 +- src/components/modules/renderer.ts | 7 +- src/components/modules/saver.ts | 3 +- src/components/utils.ts | 10 ++ test/cypress/support/commands.ts | 13 ++- test/cypress/support/index.d.ts | 9 +- test/cypress/tests/api/blocks.spec.ts | 53 ++++++++++ test/cypress/tests/block-ids.spec.ts | 130 +++++++++++++++++++++++++ test/cypress/tests/copy-paste.spec.ts | 3 +- types/api/block.d.ts | 5 + types/api/blocks.d.ts | 6 ++ types/data-formats/block-data.d.ts | 2 + types/data-formats/output-data.d.ts | 8 +- yarn.lock | 5 + 21 files changed, 333 insertions(+), 18 deletions(-) create mode 100644 test/cypress/tests/api/blocks.spec.ts create mode 100644 test/cypress/tests/block-ids.spec.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 37e47f7a..3d567102 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +### 2.21.0 +- `New` - Blocks now have a unique ID [#873](https://github.com/codex-team/editor.js/issues/873) + ### 2.20.2 - `Fix` — Append default Tunes if user tunes are provided for Block Tool [#1640](https://github.com/codex-team/editor.js/issues/1640) diff --git a/example/example-dev.html b/example/example-dev.html index 824bc4f5..cc9123f7 100644 --- a/example/example-dev.html +++ b/example/example-dev.html @@ -197,6 +197,7 @@ data: { blocks: [ { + id: "zcKCF1S7X8", type: "header", data: { text: "Editor.js", @@ -205,12 +206,14 @@ }, { type : 'paragraph', + id: "b6ji-DvaKb", data : { text : 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.' } }, { type: "header", + id: "7ItVl5biRo", data: { text: "Key features", level: 3 @@ -218,6 +221,7 @@ }, { type : 'list', + id: "SSBSguGvP7", data : { items : [ { @@ -238,6 +242,7 @@ }, { type: "header", + id: "QZFox1m_ul", data: { text: "What does it mean «block-styled editor»", level: 3 @@ -245,18 +250,21 @@ }, { type : 'paragraph', + id: "bwnFX5LoX7", data : { text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.' } }, { type : 'paragraph', + id: "mTrPOHAQTe", data : { text : `There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.` } }, { type: "header", + id: "1sYMhUrznu", data: { text: "What does it mean clean data output", level: 3 @@ -264,34 +272,40 @@ }, { type : 'paragraph', + id: "jpd7WEXrJG", data : { text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below' } }, { type : 'paragraph', + id: "0lOGNUKxqt", data : { text : `Given data can be used as you want: render with HTML for Web clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on.` } }, { type : 'paragraph', + id: "WvX7kBjp0I", data : { text : 'Clean data is useful to sanitize, validate and process on the backend.' } }, { type : 'delimiter', + id: "H9LWKQ3NYd", data : {} }, { type : 'paragraph', + id: "h298akk2Ad", data : { text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏' } }, { type: 'image', + id: "9802bjaAA2", data: { url: 'assets/codex2x.png', caption: '', diff --git a/package.json b/package.json index d877489b..431ce100 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "svg": "svg-sprite-generate -d src/assets/ -o dist/sprite.svg", "pull_tools": "git submodule update --init --recursive", "checkout_tools": "git submodule foreach git pull origin master", - "test:e2e": "cypress run" + "test:e2e": "yarn build && cypress run" }, "author": "CodeX", "license": "Apache-2.0", @@ -87,6 +87,7 @@ }, "dependencies": { "codex-notifier": "^1.1.2", - "codex-tooltip": "^1.0.2" + "codex-tooltip": "^1.0.2", + "nanoid": "^3.1.22" } } diff --git a/src/components/block/api.ts b/src/components/block/api.ts index 34e23d89..556323ee 100644 --- a/src/components/block/api.ts +++ b/src/components/block/api.ts @@ -14,6 +14,14 @@ function BlockAPI( block: Block ): void { const blockAPI: BlockAPIInterface = { + /** + * Block id + * + * @returns {string} + */ + get id(): string { + return block.id; + }, /** * Tool name * diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 4d65bdc4..18439c9d 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -23,6 +23,11 @@ import ToolsCollection from '../tools/collection'; * Interface describes Block class constructor argument */ interface BlockConstructorOptions { + /** + * Block's id. Should be passed for existed block, and omitted for a new one. + */ + id?: string; + /** * Initial Block data */ @@ -98,6 +103,11 @@ export default class Block { }; } + /** + * Block unique identifier + */ + public id: string; + /** * Block Tool`s name */ @@ -206,13 +216,14 @@ export default class Block { /** * @param {object} options - block constructor options + * @param {string} [options.id] - block's id. Will be generated if omitted. * @param {BlockToolData} options.data - Tool's initial data - * @param {BlockToolConstructable} options.Tool — Tool's class - * @param {ToolSettings} options.settings - default tool's config + * @param {BlockToolConstructable} options.tool — block's tool * @param options.api - Editor API module for pass it to the Block Tunes * @param {boolean} options.readOnly - Read-Only flag */ constructor({ + id = _.generateBlockId(), data, tool, api, @@ -220,6 +231,7 @@ export default class Block { tunesData, }: BlockConstructorOptions) { this.name = tool.name; + this.id = id; this.settings = tool.settings; this.config = tool.settings.config || {}; this.api = api; @@ -567,6 +579,7 @@ export default class Block { measuringEnd = window.performance.now(); return { + id: this.id, tool: this.name, data: finishedExtraction, tunes: tunesData, diff --git a/src/components/modules/api/blocks.ts b/src/components/modules/api/blocks.ts index a65db8b0..7da432e4 100644 --- a/src/components/modules/api/blocks.ts +++ b/src/components/modules/api/blocks.ts @@ -23,6 +23,7 @@ export default class BlocksAPI extends Module { swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex), move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex), getBlockByIndex: (index: number): BlockAPIInterface | void => this.getBlockByIndex(index), + getById: (id: string): BlockAPIInterface | null => this.getById(id), getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(), getBlocksCount: (): number => this.getBlocksCount(), stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status), @@ -66,6 +67,23 @@ export default class BlocksAPI extends Module { return new BlockAPI(block); } + /** + * Returns BlockAPI object by Block id + * + * @param id - id of block to get + */ + public getById(id: string): BlockAPIInterface | null { + const block = this.Editor.BlockManager.getBlockById(id); + + if (block === undefined) { + _.logLabeled('There is no block with id `' + id + '`', 'warn'); + + return null; + } + + return new BlockAPI(block); + } + /** * Call Block Manager method that swap Blocks * diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 5614aa83..e82ba86f 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -216,6 +216,7 @@ export default class BlockManager extends Module { * * @param {object} options - block creation options * @param {string} options.tool - tools passed in editor config {@link EditorConfig#tools} + * @param {string} [options.id] - unique id for this block * @param {BlockToolData} [options.data] - constructor params * * @returns {Block} @@ -223,11 +224,13 @@ export default class BlockManager extends Module { public composeBlock({ tool: name, data = {}, + id = undefined, tunes: tunesData = {}, - }: {tool: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { + }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block { const readOnly = this.Editor.ReadOnly.isEnabled; const tool = this.Editor.Tools.blockTools.get(name); const block = new Block({ + id, data, tool, api: this.Editor.API, @@ -246,15 +249,17 @@ export default class BlockManager extends Module { * Insert new block into _blocks * * @param {object} options - insert options - * @param {string} options.tool - plugin name, by default method inserts the default block type - * @param {object} options.data - plugin data - * @param {number} options.index - index where to insert new Block - * @param {boolean} options.needToFocus - flag shows if needed to update current Block index - * @param {boolean} options.replace - flag shows if block by passed index should be replaced with inserted one + * @param {string} [options.id] - block's unique id + * @param {string} [options.tool] - plugin name, by default method inserts the default block type + * @param {object} [options.data] - plugin data + * @param {number} [options.index] - index where to insert new Block + * @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index + * @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one * * @returns {Block} */ public insert({ + id = undefined, tool = this.config.defaultBlock, data = {}, index, @@ -262,6 +267,7 @@ export default class BlockManager extends Module { replace = false, tunes = {}, }: { + id?: string; tool?: string; data?: BlockToolData; index?: number; @@ -276,6 +282,7 @@ export default class BlockManager extends Module { } const block = this.composeBlock({ + id, tool, data, tunes, @@ -514,6 +521,17 @@ export default class BlockManager extends Module { return this._blocks[index]; } + /** + * Returns the Block by passed id + * + * @param id - id of block to get + * + * @returns {Block} + */ + public getBlockById(id): Block { + return this._blocks.array.find(block => block.id === id); + } + /** * Get Block instance by html element * diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts index a9280bd0..61f794af 100644 --- a/src/components/modules/paste.ts +++ b/src/components/modules/paste.ts @@ -739,7 +739,7 @@ export default class Paste extends Module { * * @returns {void} */ - private insertEditorJSData(blocks: Pick[]): void { + private insertEditorJSData(blocks: Pick[]): void { const { BlockManager, Caret, Tools } = this.Editor; const sanitizedBlocks = sanitizeBlocks(blocks, (name) => Tools.blockTools.get(name).sanitizeConfig diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index b9651b01..0e87e751 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -23,12 +23,14 @@ export default class Renderer extends Module { * * blocks: [ * { + * id : 'oDe-EVrGWA', * type : 'paragraph', * data : { * text : 'Hello from Codex!' * } * }, * { + * id : 'Ld5BJjJCHs', * type : 'paragraph', * data : { * text : 'Leave feedback if you like it!' @@ -64,11 +66,12 @@ export default class Renderer extends Module { */ public async insertBlock(item: OutputBlockData): Promise { const { Tools, BlockManager } = this.Editor; - const { type: tool, data, tunes } = item; + const { type: tool, data, tunes, id } = item; if (Tools.available.has(tool)) { try { BlockManager.insert({ + id, tool, data, tunes, @@ -81,6 +84,7 @@ export default class Renderer extends Module { /** If Tool is unavailable, create stub Block for it */ const stubData = { savedData: { + id, type: tool, data, }, @@ -94,6 +98,7 @@ export default class Renderer extends Module { } const stub = BlockManager.insert({ + id, tool: Tools.stubTool, data: stubData, }); diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts index ba3cfd8c..521dbda5 100644 --- a/src/components/modules/saver.ts +++ b/src/components/modules/saver.ts @@ -81,7 +81,7 @@ export default class Saver extends Module { _.log('[Editor.js saving]:', 'groupCollapsed'); - allExtractedData.forEach(({ tool, data, tunes, time, isValid }) => { + allExtractedData.forEach(({ id, tool, data, tunes, time, isValid }) => { totalTime += time; /** @@ -108,6 +108,7 @@ export default class Saver extends Module { } const output = { + id, type: tool, data, ...!_.isEmpty(tunes) && { diff --git a/src/components/utils.ts b/src/components/utils.ts index 5a82dec7..4cc92a31 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -2,6 +2,7 @@ * Class Util */ +import { nanoid } from 'nanoid'; import Dom from './dom'; /** @@ -607,6 +608,15 @@ export function getValidUrl(url: string): string { } } +/** + * Create a block id + * + * @returns {string} + */ +export function generateBlockId(): string { + return nanoid(10); +} + /** * Opens new Tab with passed URL * diff --git a/test/cypress/support/commands.ts b/test/cypress/support/commands.ts index fc829c09..fe250e4a 100644 --- a/test/cypress/support/commands.ts +++ b/test/cypress/support/commands.ts @@ -5,7 +5,7 @@ * -------------------------------------------------- */ -import type { EditorConfig } from './../../../types/index'; +import type { EditorConfig, OutputData } from './../../../types/index'; import type EditorJS from '../../../types/index'; import Chainable = Cypress.Chainable; @@ -114,3 +114,14 @@ Cypress.Commands.add('cut', { prevSubject: true }, async (subject) => { return clipboardData; }); + +/** + * Calls EditorJS API render method + * + * @param data — data to render + */ +Cypress.Commands.add('render', { prevSubject: true }, async (subject: EditorJS, data: OutputData): Promise => { + await subject.render(data); + + return subject; +}); diff --git a/test/cypress/support/index.d.ts b/test/cypress/support/index.d.ts index 7d83933e..477e3e7d 100644 --- a/test/cypress/support/index.d.ts +++ b/test/cypress/support/index.d.ts @@ -2,7 +2,7 @@ // load type definitions that come with Cypress module /// -import type { EditorConfig } from './../../../types/index'; +import type { EditorConfig, OutputData } from './../../../types/index'; import type EditorJS from '../../../types/index' declare global { @@ -40,6 +40,13 @@ declare global { * cy.get('div').cut().then(data => {}) */ cut(): Chainable<{ [type: string]: any }>; + + /** + * Calls EditorJS API render method + * + * @param data — data to render + */ + render(data: OutputData): Chainable; } interface ApplicationWindow { diff --git a/test/cypress/tests/api/blocks.spec.ts b/test/cypress/tests/api/blocks.spec.ts new file mode 100644 index 00000000..db14d9a2 --- /dev/null +++ b/test/cypress/tests/api/blocks.spec.ts @@ -0,0 +1,53 @@ +/** + * There will be described test cases of 'blocks.*' API + */ +describe('api.blocks', () => { + const firstBlock = { + id: 'bwnFX5LoX7', + type: 'paragraph', + data: { + text: 'The first block content mock.', + }, + }; + const editorDataMock = { + blocks: [ + firstBlock, + ], + }; + + beforeEach(() => { + if (this && this.editorInstance) { + this.editorInstance.destroy(); + } else { + cy.createEditor({ + data: editorDataMock, + }).as('editorInstance'); + } + }); + + /** + * api.blocks.getById(id) + */ + describe('.getById()', () => { + /** + * Check that api.blocks.getByUd(id) returns the Block for existed id + */ + it('should return Block API for existed id', () => { + cy.get('@editorInstance').then(async (editor: any) => { + const block = editor.blocks.getById(firstBlock.id); + + expect(block).not.to.be.undefined; + expect(block.id).to.be.eq(firstBlock.id); + }); + }); + + /** + * Check that api.blocks.getByUd(id) returns null for the not-existed id + */ + it('should return null for not-existed id', () => { + cy.get('@editorInstance').then(async (editor: any) => { + expect(editor.blocks.getById('not-existed-id')).to.be.null; + }); + }); + }); +}); diff --git a/test/cypress/tests/block-ids.spec.ts b/test/cypress/tests/block-ids.spec.ts new file mode 100644 index 00000000..3486f789 --- /dev/null +++ b/test/cypress/tests/block-ids.spec.ts @@ -0,0 +1,130 @@ +import Header from '../../../example/tools/header'; +import { nanoid } from 'nanoid'; + +describe.only('Block ids', () => { + beforeEach(() => { + if (this && this.editorInstance) { + this.editorInstance.destroy(); + } else { + cy.createEditor({ + tools: { + header: Header, + }, + }).as('editorInstance'); + } + }); + + it('Should generate unique block ids for new blocks', () => { + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .click() + .type('First block ') + .type('{enter}') + .get('div.ce-block') + .last() + .type('Second block ') + .type('{enter}'); + + cy.get('[data-cy=editorjs]') + .get('div.ce-toolbar__plus') + .click(); + + cy.get('[data-cy=editorjs]') + .get('li.ce-toolbox__button[data-tool=header]') + .click(); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .last() + .click() + .type('Header'); + + cy.get('@editorInstance') + .then(async (editor: any) => { + const data = await editor.save(); + + data.blocks.forEach(block => { + expect(typeof block.id).to.eq('string'); + }); + }); + }); + + it('should preserve passed ids', () => { + const blocks = [ + { + id: nanoid(), + type: 'paragraph', + data: { + text: 'First block', + }, + }, + { + id: nanoid(), + type: 'paragraph', + data: { + text: 'Second block', + }, + }, + ]; + + cy.get('@editorInstance') + .render({ + blocks, + }); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .first() + .click() + .type('{movetoend} Some more text'); + + cy.get('@editorInstance') + .then(async (editor: any) => { + const data = await editor.save(); + + data.blocks.forEach((block, index) => { + expect(block.id).to.eq(blocks[index].id); + }); + }); + }); + + it('should preserve passed ids if blocks were added', () => { + const blocks = [ + { + id: nanoid(), + type: 'paragraph', + data: { + text: 'First block', + }, + }, + { + id: nanoid(), + type: 'paragraph', + data: { + text: 'Second block', + }, + }, + ]; + + cy.get('@editorInstance') + .render({ + blocks, + }); + + cy.get('[data-cy=editorjs]') + .get('div.ce-block') + .first() + .click() + .type('{enter}') + .next() + .type('Middle block'); + + cy.get('@editorInstance') + .then(async (editor: any) => { + const data = await editor.save(); + + expect(data.blocks[0].id).to.eq(blocks[0].id); + expect(data.blocks[2].id).to.eq(blocks[1].id); + }); + }); +}); diff --git a/test/cypress/tests/copy-paste.spec.ts b/test/cypress/tests/copy-paste.spec.ts index beae0177..5ec8e703 100644 --- a/test/cypress/tests/copy-paste.spec.ts +++ b/test/cypress/tests/copy-paste.spec.ts @@ -126,7 +126,8 @@ describe('Copy pasting from Editor', () => { }); cy.get('[data-cy=editorjs]') - .get('img') + // In Edge test are performed slower, so we need to increase timeout to wait until image is loaded on the page + .get('img', { timeout: 10000 }) .should('have.attr', 'src', 'https://codex.so/public/app/img/external/codex2x.png'); }); }); diff --git a/types/api/block.d.ts b/types/api/block.d.ts index cb3ca293..81ba721f 100644 --- a/types/api/block.d.ts +++ b/types/api/block.d.ts @@ -5,6 +5,11 @@ import {SavedData} from '../data-formats'; * @interface BlockAPI Describes Block API methods and properties */ export interface BlockAPI { + /** + * Block unique identifier + */ + readonly id: string; + /** * Tool name */ diff --git a/types/api/blocks.d.ts b/types/api/blocks.d.ts index b933a817..fa4c11db 100644 --- a/types/api/blocks.d.ts +++ b/types/api/blocks.d.ts @@ -54,6 +54,12 @@ export interface Blocks { */ getBlockByIndex(index: number): BlockAPI | void; + /** + * Returns Block API object by passed Block id + * @param id - id of the block + */ + getById(id: string): BlockAPI | null; + /** * Returns current Block index * @returns {number} diff --git a/types/data-formats/block-data.d.ts b/types/data-formats/block-data.d.ts index 063b63b6..f4843a47 100644 --- a/types/data-formats/block-data.d.ts +++ b/types/data-formats/block-data.d.ts @@ -4,6 +4,7 @@ import {BlockToolData} from '../tools'; * Tool's saved data */ export interface SavedData { + id: string; tool: string; data: BlockToolData; time: number; @@ -13,6 +14,7 @@ export interface SavedData { * Tool's data after validation */ export interface ValidatedData { + id?: string; tool?: string; data?: BlockToolData; time?: number; diff --git a/types/data-formats/output-data.d.ts b/types/data-formats/output-data.d.ts index 05961e1f..07f296e1 100644 --- a/types/data-formats/output-data.d.ts +++ b/types/data-formats/output-data.d.ts @@ -1,5 +1,5 @@ import {BlockToolData} from '../tools'; -import {BlockTuneData} from "../block-tunes/block-tune-data"; +import {BlockTuneData} from '../block-tunes/block-tune-data'; /** * Output of one Tool @@ -8,6 +8,10 @@ import {BlockTuneData} from "../block-tunes/block-tune-data"; * @template Data - the structure describing a data object supported by the tool */ export interface OutputBlockData { + /** + * Unique Id of the block + */ + id?: string; /** * Tool type */ @@ -18,7 +22,7 @@ export interface OutputBlockData; /** - * Block Tunes data + * Block Tunes data */ tunes?: {[name: string]: BlockTuneData}; } diff --git a/yarn.lock b/yarn.lock index d578f65d..cbaba152 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6248,6 +6248,11 @@ nanoid@3.1.20: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== +nanoid@^3.1.22: + version "3.1.22" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" + integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"