diff --git a/cypress.config.ts b/cypress.config.ts index ed2c10f03..11ac2de20 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -11,9 +11,22 @@ export default defineConfig({ // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. setupNodeEvents(on, config) { - return require('./test/cypress/plugins/index.ts')(on, config); + /** + * Plugin for cypress that adds better terminal output for easier debugging. + * Prints cy commands, browser console logs, cy.request and cy.intercept data. Great for your pipelines. + * https://github.com/archfz/cypress-terminal-report + */ + require('cypress-terminal-report/src/installLogsPrinter')(on); + + require('./test/cypress/plugins/index.ts')(on, config); }, specPattern: 'test/cypress/tests/**/*.cy.{js,jsx,ts,tsx}', supportFile: 'test/cypress/support/index.ts', }, + 'retries': { + // Configure retry attempts for `cypress run` + 'runMode': 2, + // Configure retry attempts for `cypress open` + 'openMode': 0, + }, }); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2a54eaba6..0d07fbbb1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,7 @@ - `Improvement` - Selection style won't override your custom style for `::selection` outside the editor. - `Improvement` - Performance optimizations: initialization speed increased, `blocks.render()` API method optimized. Big documents will be displayed faster. - `Improvement` - "Editor saving" log removed +- `Improvement` - "I'm ready" log removed ### 2.27.2 diff --git a/package.json b/package.json index f97c16ba8..4854784ec 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "core-js": "3.30.0", "cypress": "^12.9.0", "cypress-intellij-reporter": "^0.0.7", + "cypress-terminal-report": "^5.3.2", "eslint": "^8.37.0", "eslint-config-codex": "^1.7.1", "eslint-plugin-chai-friendly": "^0.7.2", diff --git a/src/components/core.ts b/src/components/core.ts index 99f96d102..6c2674e28 100644 --- a/src/components/core.ts +++ b/src/components/core.ts @@ -54,8 +54,6 @@ export default class Core { this.init(); await this.start(); - _.logLabeled('I\'m ready! (ノ◕ヮ◕)ノ*:・゚✧', 'log', '', 'color: #E24A75'); - await this.render(); if ((this.configuration as EditorConfig).autofocus) { @@ -68,7 +66,9 @@ export default class Core { /** * Resolve this.isReady promise */ - onReady(); + window.requestIdleCallback(() => { + onReady(); + }); }) .catch((error) => { _.log(`Editor.js is not ready because of ${error}`, 'error'); diff --git a/src/components/modules/caret.ts b/src/components/modules/caret.ts index 0494dcaf2..a13903e4d 100644 --- a/src/components/modules/caret.ts +++ b/src/components/modules/caret.ts @@ -534,7 +534,7 @@ export default class Caret extends Module { fragment.appendChild(new Text()); } - const lastChild = fragment.lastChild; + const lastChild = fragment.lastChild as ChildNode; range.deleteContents(); range.insertNode(fragment); @@ -542,7 +542,11 @@ export default class Caret extends Module { /** Cross-browser caret insertion */ const newRange = document.createRange(); - newRange.setStart(lastChild, lastChild.textContent.length); + const nodeToSetCaret = lastChild.nodeType === Node.TEXT_NODE ? lastChild : lastChild.firstChild; + + if (nodeToSetCaret !== null && nodeToSetCaret.textContent !== null) { + newRange.setStart(nodeToSetCaret, nodeToSetCaret.textContent.length); + } selection.removeAllRanges(); selection.addRange(newRange); diff --git a/src/components/modules/renderer.ts b/src/components/modules/renderer.ts index b078f202b..2a32a54fa 100644 --- a/src/components/modules/renderer.ts +++ b/src/components/modules/renderer.ts @@ -22,6 +22,11 @@ export default class Renderer extends Module { * Create Blocks instances */ const blocks = blocksData.map(({ type: tool, data, tunes, id }) => { + /** + * @todo handle plugin error + * @todo handle stub case + */ + return this.Editor.BlockManager.composeBlock({ id, tool, @@ -30,6 +35,8 @@ export default class Renderer extends Module { }); }); + + /** * Insert batch of Blocks */ @@ -48,6 +55,8 @@ export default class Renderer extends Module { */ this.Editor.ModificationsObserver.enable(); }, { timeout: 2000 }); + + } /** diff --git a/src/components/modules/toolbar/index.ts b/src/components/modules/toolbar/index.ts index db2e33b4e..6f822380a 100644 --- a/src/components/modules/toolbar/index.ts +++ b/src/components/modules/toolbar/index.ts @@ -103,8 +103,9 @@ export default class Toolbar extends Module { /** * Toolbox class instance + * It will be created in requestIdleCallback so it can be null in some period of time */ - private toolboxInstance: Toolbox; + private toolboxInstance: Toolbox | null = null; /** * @class @@ -155,18 +156,27 @@ export default class Toolbar extends Module { * Public interface for accessing the Toolbox */ public get toolbox(): { - opened: boolean; + opened: boolean | undefined; // undefined is for the case when Toolbox is not initialized yet close: () => void; open: () => void; toggle: () => void; - hasFocus: () => boolean; + hasFocus: () => boolean | undefined; } { return { - opened: this.toolboxInstance.opened, - close: (): void => { - this.toolboxInstance.close(); + opened: this.toolboxInstance?.opened, + close: () => { + this.toolboxInstance?.close(); }, - open: (): void => { + open: () => { + /** + * If Toolbox is not initialized yet, do nothing + */ + if (this.toolboxInstance === null) { + _.log('toolbox.open() called before initialization is finished', 'warn'); + + return; + } + /** * Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block. */ @@ -174,8 +184,19 @@ export default class Toolbar extends Module { this.toolboxInstance.open(); }, - toggle: (): void => this.toolboxInstance.toggle(), - hasFocus: (): boolean => this.toolboxInstance.hasFocus(), + toggle: () => { + /** + * If Toolbox is not initialized yet, do nothing + */ + if (this.toolboxInstance === null) { + _.log('toolbox.toggle() called before initialization is finished', 'warn'); + + return; + } + + this.toolboxInstance.toggle(); + }, + hasFocus: () => this.toolboxInstance?.hasFocus(), }; } @@ -227,6 +248,15 @@ export default class Toolbar extends Module { * @param block - block to move Toolbar near it */ public moveAndOpen(block: Block = this.Editor.BlockManager.currentBlock): void { + /** + * Some UI elements creates inside requestIdleCallback, so the can be not ready yet + */ + if (this.toolboxInstance === null) { + _.log('Can\'t open Toolbar since Editor initialization is not finished yet', 'warn'); + + return; + } + /** * Close Toolbox when we move toolbar */ @@ -296,7 +326,7 @@ export default class Toolbar extends Module { /** Close components */ this.blockActions.hide(); - this.toolboxInstance.close(); + this.toolboxInstance?.close(); this.Editor.BlockSettings.close(); } @@ -456,7 +486,7 @@ export default class Toolbar extends Module { */ this.Editor.BlockManager.currentBlock = this.hoveredBlock; - this.toolboxInstance.toggle(); + this.toolboxInstance?.toggle(); } /** @@ -478,7 +508,7 @@ export default class Toolbar extends Module { this.settingsTogglerClicked(); - if (this.toolboxInstance.opened) { + if (this.toolboxInstance?.opened) { this.toolboxInstance.close(); } @@ -498,7 +528,7 @@ export default class Toolbar extends Module { /** * Do not move toolbar if Block Settings or Toolbox opened */ - if (this.Editor.BlockSettings.opened || this.toolboxInstance.opened) { + if (this.Editor.BlockSettings.opened || this.toolboxInstance?.opened) { return; } diff --git a/src/components/selection.ts b/src/components/selection.ts index d38dd841a..fe5f961a8 100644 --- a/src/components/selection.ts +++ b/src/components/selection.ts @@ -225,7 +225,7 @@ export default class SelectionUtils { * * @param selection - Selection object to get Range from */ - public static getRangeFromSelection(selection: Selection): Range { + public static getRangeFromSelection(selection: Selection): Range | null { return selection && selection.rangeCount ? selection.getRangeAt(0) : null; } diff --git a/src/components/utils/events.ts b/src/components/utils/events.ts index 3c748a2c1..2599f0b74 100644 --- a/src/components/utils/events.ts +++ b/src/components/utils/events.ts @@ -94,6 +94,12 @@ export default class EventsDispatcher { * @param callback - event handler */ public off(eventName: Name, callback: Listener): void { + if (this.subscribers[eventName] === undefined) { + console.warn(`EventDispatcher .off(): there is no subscribers for event "${eventName.toString()}". Probably, .off() called before .on()`); + + return; + } + for (let i = 0; i < this.subscribers[eventName].length; i++) { if (this.subscribers[eventName][i] === callback) { delete this.subscribers[eventName][i]; @@ -107,6 +113,6 @@ export default class EventsDispatcher { * clears subscribers list */ public destroy(): void { - this.subscribers = null; + this.subscribers = {} as Subscriptions; } } diff --git a/test/cypress/.eslintrc b/test/cypress/.eslintrc index 8b90cf869..6671eb309 100644 --- a/test/cypress/.eslintrc +++ b/test/cypress/.eslintrc @@ -11,7 +11,9 @@ "plugin:chai-friendly/recommended" ], "rules": { - "cypress/require-data-selectors": 2 + "cypress/require-data-selectors": 2, + "cypress/no-unnecessary-waiting": 0, + "@typescript-eslint/no-magic-numbers": 0 }, "globals": { "EditorJS": true diff --git a/test/cypress/support/commands.ts b/test/cypress/support/commands.ts index 8f19ca2e4..3feaceaca 100644 --- a/test/cypress/support/commands.ts +++ b/test/cypress/support/commands.ts @@ -61,7 +61,7 @@ Cypress.Commands.add('paste', { subject[0].dispatchEvent(pasteEvent); - return subject; + cy.wait(200); // wait a little since some tools (paragraph) could have async hydration }); /** diff --git a/test/cypress/support/index.ts b/test/cypress/support/index.ts index 6eb582ec0..a3a0662ad 100644 --- a/test/cypress/support/index.ts +++ b/test/cypress/support/index.ts @@ -7,6 +7,9 @@ */ import '@cypress/code-coverage/support'; +import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector'; + +installLogsCollector(); /** * File with the helpful commands diff --git a/test/cypress/tests/api/blocks.cy.ts b/test/cypress/tests/api/blocks.cy.ts index 62c49fcc9..0218fcdd2 100644 --- a/test/cypress/tests/api/blocks.cy.ts +++ b/test/cypress/tests/api/blocks.cy.ts @@ -214,7 +214,7 @@ describe('api.blocks', () => { convert(existingBlock.id, 'convertableTool'); }); - // eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers -- wait for block to be converted + // wait for block to be converted cy.wait(100); /** diff --git a/test/cypress/tests/block-ids.cy.ts b/test/cypress/tests/block-ids.cy.ts index 19046137d..910fe1ed8 100644 --- a/test/cypress/tests/block-ids.cy.ts +++ b/test/cypress/tests/block-ids.cy.ts @@ -75,12 +75,6 @@ describe('Block ids', () => { 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(); diff --git a/test/cypress/tests/copy-paste.cy.ts b/test/cypress/tests/copy-paste.cy.ts index 87011d047..132c1c908 100644 --- a/test/cypress/tests/copy-paste.cy.ts +++ b/test/cypress/tests/copy-paste.cy.ts @@ -1,51 +1,44 @@ import Header from '@editorjs/header'; import Image from '@editorjs/simple-image'; import * as _ from '../../../src/components/utils'; -import EditorJS, { BlockTool, BlockToolData } from '../../../types'; +import { BlockTool, BlockToolData } from '../../../types'; import $ from '../../../src/components/dom'; describe('Copy pasting from Editor', function () { - beforeEach(function () { - cy.createEditor({ - tools: { - header: Header, - image: Image, - }, - }).as('editorInstance'); - }); - - afterEach(function () { - if (this.editorInstance && this.editorInstance.destroy) { - this.editorInstance.destroy(); - } - }); - context('pasting', function () { it('should paste plain text', function () { - // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') + .as('block') .click() .paste({ // eslint-disable-next-line @typescript-eslint/naming-convention 'text/plain': 'Some plain text', - }) - .wait(0) - .should('contain', 'Some plain text'); + }); + + cy.get('@block').should('contain', 'Some plain text'); }); it('should paste inline html data', function () { + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') + .as('block') .click() .paste({ // eslint-disable-next-line @typescript-eslint/naming-convention 'text/html': '

Some text

', - }) - .should('contain.html', 'Some text'); + }); + + cy.get('@block').should('contain.html', 'Some text'); }); it('should paste several blocks if plain text contains new lines', function () { + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -63,6 +56,8 @@ describe('Copy pasting from Editor', function () { }); it('should paste several blocks if html contains several paragraphs', function () { + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -80,6 +75,8 @@ describe('Copy pasting from Editor', function () { }); it('should paste using custom data type', function () { + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -110,6 +107,12 @@ describe('Copy pasting from Editor', function () { }); it('should parse block tags', function () { + cy.createEditor({ + tools: { + header: Header, + }, + }); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -128,6 +131,12 @@ describe('Copy pasting from Editor', function () { }); it('should parse pattern', function () { + cy.createEditor({ + tools: { + image: Image, + }, + }); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -143,12 +152,6 @@ describe('Copy pasting from Editor', function () { }); it('should not prevent default behaviour if block\'s paste config equals false', function () { - /** - * Destroy default Editor to render custom one with different tools - */ - cy.get('@editorInstance') - .then((editorInstance: unknown) => (editorInstance as EditorJS).destroy()); - const onPasteStub = cy.stub().as('onPaste'); /** @@ -182,7 +185,8 @@ describe('Copy pasting from Editor', function () { tools: { blockToolWithPasteHandler: BlockToolWithPasteHandler, }, - }).as('editorInstanceWithBlockToolWithPasteHandler'); + }) + .as('editorInstanceWithBlockToolWithPasteHandler'); cy.get('@editorInstanceWithBlockToolWithPasteHandler') .render({ @@ -192,7 +196,8 @@ describe('Copy pasting from Editor', function () { data: {}, }, ], - }); + }) + .wait(100); cy.get('@editorInstanceWithBlockToolWithPasteHandler') .get('div.ce-block-with-disabled-prevent-default') @@ -211,6 +216,8 @@ describe('Copy pasting from Editor', function () { context('copying', function () { it('should copy inline fragment', function () { + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -225,6 +232,8 @@ describe('Copy pasting from Editor', function () { }); it('should copy several blocks', function () { + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -247,7 +256,6 @@ describe('Copy pasting from Editor', function () { /** * Need to wait for custom data as it is set asynchronously */ - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(0).then(function () { expect(clipboardData['application/x-editor-js']).not.to.be.undefined; @@ -264,6 +272,8 @@ describe('Copy pasting from Editor', function () { context('cutting', function () { it('should cut inline fragment', function () { + cy.createEditor({}); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -278,15 +288,25 @@ describe('Copy pasting from Editor', function () { }); it('should cut several blocks', function () { - cy.get('[data-cy=editorjs]') - .get('div.ce-block') - .click() - .type('First block{enter}'); + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { text: 'First block' }, + }, + { + type: 'paragraph', + data: { text: 'Second block' }, + }, + ], + }, + }); cy.get('[data-cy=editorjs') .get('div.ce-block') - .next() - .type('Second block') + .last() + .click() .type('{movetostart}') .trigger('keydown', { shiftKey: true, @@ -300,7 +320,6 @@ describe('Copy pasting from Editor', function () { /** * Need to wait for custom data as it is set asynchronously */ - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(0).then(function () { expect(clipboardData['application/x-editor-js']).not.to.be.undefined; @@ -320,15 +339,23 @@ describe('Copy pasting from Editor', function () { it('should cut lots of blocks', function () { const numberOfBlocks = 50; + const blocks = []; for (let i = 0; i < numberOfBlocks; i++) { - cy.get('[data-cy=editorjs]') - .get('div.ce-block') - .last() - .click() - .type(`Block ${i}{enter}`); + blocks.push({ + type: 'paragraph', + data: { + text: `Block ${i}`, + }, + }); } + cy.createEditor({ + data: { + blocks, + }, + }); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .first() @@ -340,13 +367,12 @@ describe('Copy pasting from Editor', function () { /** * Need to wait for custom data as it is set asynchronously */ - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(0).then(function () { expect(clipboardData['application/x-editor-js']).not.to.be.undefined; const data = JSON.parse(clipboardData['application/x-editor-js']); - expect(data.length).to.eq(numberOfBlocks + 1); + expect(data.length).to.eq(numberOfBlocks); }); }); }); diff --git a/test/cypress/tests/i18n.cy.ts b/test/cypress/tests/i18n.cy.ts index 621bd25ea..cec4f2c04 100644 --- a/test/cypress/tests/i18n.cy.ts +++ b/test/cypress/tests/i18n.cy.ts @@ -127,7 +127,8 @@ describe('Editor i18n', () => { toolNames: toolNamesDictionary, }, }, - }).as('editorInstance'); + }); + cy.get('[data-cy=editorjs]') .get('div.ce-block') .click(); diff --git a/test/cypress/tests/modules/BlockEvents/Enter.cy.ts b/test/cypress/tests/modules/BlockEvents/Enter.cy.ts index 7469d1878..3dde743e3 100644 --- a/test/cypress/tests/modules/BlockEvents/Enter.cy.ts +++ b/test/cypress/tests/modules/BlockEvents/Enter.cy.ts @@ -13,8 +13,6 @@ describe('Enter keydown', function () { }, }); - - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.get('[data-cy=editorjs]') .find('.ce-paragraph') .click() @@ -22,7 +20,6 @@ describe('Enter keydown', function () { .wait(0) .type('{enter}'); - cy.get('[data-cy=editorjs]') .find('div.ce-block') .then((blocks) => { diff --git a/test/cypress/tests/onchange.cy.ts b/test/cypress/tests/onchange.cy.ts index 13c4fccfa..5c66f67ff 100644 --- a/test/cypress/tests/onchange.cy.ts +++ b/test/cypress/tests/onchange.cy.ts @@ -132,7 +132,6 @@ describe('onChange callback', () => { }, ]); - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.get('[data-cy=editorjs]') .get('div.ce-block') .click() @@ -483,7 +482,6 @@ describe('onChange callback', () => { .get('div.ce-block') .click(); - // eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers cy.wait(500).then(() => { cy.get('@onChange').should('have.callCount', 0); }); @@ -562,7 +560,6 @@ describe('onChange callback', () => { /** * Emulate tool's internal attribute mutation */ - // eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers cy.wait(100).then(() => { toolWrapper.setAttribute('some-changed-attr', 'some-new-value'); }); @@ -570,7 +567,6 @@ describe('onChange callback', () => { /** * Check that onChange callback was not called */ - // eslint-disable-next-line cypress/no-unnecessary-waiting, @typescript-eslint/no-magic-numbers cy.wait(500).then(() => { cy.get('@onChange').should('have.callCount', 0); }); diff --git a/yarn.lock b/yarn.lock index d490696b9..6c53942c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1649,6 +1649,17 @@ cypress-intellij-reporter@^0.0.7: dependencies: mocha latest +cypress-terminal-report@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/cypress-terminal-report/-/cypress-terminal-report-5.3.2.tgz#3a6b1cbda6101498243d17c5a2a646cb69af0336" + integrity sha512-0Gf/pXjrYpTkf2aR3LAFGoxEM0KulWsMKCu+52YJB6l7GEP2RLAOAr32tcZHZiL2EWnS0vE4ollomMzGvCci0w== + dependencies: + chalk "^4.0.0" + fs-extra "^10.1.0" + safe-json-stringify "^1.2.0" + semver "^7.3.5" + tv4 "^1.3.0" + cypress@^12.9.0: version "12.9.0" resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.9.0.tgz#e6ab43cf329fd7c821ef7645517649d72ccf0a12" @@ -4443,6 +4454,11 @@ safe-buffer@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" +safe-json-stringify@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -4474,6 +4490,13 @@ semver@^7.0.0, semver@^7.3.2, semver@^7.3.4, semver@^7.3.7, semver@^7.3.8: dependencies: lru-cache "^6.0.0" +semver@^7.3.5: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -4936,6 +4959,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tv4@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963" + integrity sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"