From 71283b351821beaff63ef60904e9b2469fd7dd99 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Mon, 6 May 2024 19:54:04 +0200 Subject: [PATCH 01/10] Prevent empty namespace or different namespaces from killing the runtime --- packages/interactivity/src/constants.js | 3 +++ packages/interactivity/src/hooks.tsx | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/interactivity/src/constants.js b/packages/interactivity/src/constants.js index 669e94263fb9c..aecf919067bdb 100644 --- a/packages/interactivity/src/constants.js +++ b/packages/interactivity/src/constants.js @@ -1 +1,4 @@ export const directivePrefix = 'wp'; + +export const isDebug = + typeof SCRIPT_DEBUG !== 'undefined' && SCRIPT_DEBUG === true; diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index f223ddceb9a41..76356f04d886e 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -16,6 +16,7 @@ import type { VNode, Context, RefObject } from 'preact'; * Internal dependencies */ import { store, stores, universalUnlock } from './store'; +import { isDebug } from './constants'; interface DirectiveEntry { value: string | Object; namespace: string; @@ -260,18 +261,35 @@ export const directive = ( // Resolve the path to some property of the store object. const resolve = ( path, namespace ) => { + if ( namespace === '' ) { + if ( isDebug ) { + // eslint-disable-next-line no-console + console.warn( + `Namespace cannot be an empty string. Error found when trying to use "${ path }"` + ); + } + return; + } let resolvedStore = stores.get( namespace ); if ( typeof resolvedStore === 'undefined' ) { resolvedStore = store( namespace, undefined, { lock: universalUnlock, } ); } - let current = { + const current = { ...resolvedStore, context: getScope().context[ namespace ], }; - path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); - return current; + try { + return path.split( '.' ).reduce( ( acc, key ) => acc[ key ], current ); + } catch ( e ) { + if ( isDebug ) { + // eslint-disable-next-line no-console + console.warn( + `The namespace "${ namespace }" defined in "data-wp-interactive" does not match with the store.` + ); + } + } }; // Generate the evaluate function. From 26e321afd697d414b1b2471a15c53559d41931d8 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Mon, 6 May 2024 20:04:03 +0200 Subject: [PATCH 02/10] Update changelog --- packages/interactivity/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 56352eb4c489b..b0fb12a494856 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -20,6 +20,8 @@ - Hooks useMemo and useCallback should return a value. ([#60474](https://github.com/WordPress/gutenberg/pull/60474)) +- Prevent empty namespace or different namespaces from killing the runtime ([#61409](https://github.com/WordPress/gutenberg/pull/61409)) + ## 5.4.0 (2024-04-03) ## 5.3.0 (2024-03-21) From 1c370136da3fdb87e19dac2059934f08a04604e5 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Tue, 7 May 2024 21:42:51 +0200 Subject: [PATCH 03/10] Move changelog --- packages/interactivity/CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index b0fb12a494856..fc22086bef443 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -7,6 +7,7 @@ - Allow multiple event handlers for the same type with `data-wp-on-document` and `data-wp-on-window`. ([#61009](https://github.com/WordPress/gutenberg/pull/61009)) - Prevent wrong written directives from killing the runtime ([#61249](https://github.com/WordPress/gutenberg/pull/61249)) +- Prevent empty namespace or different namespaces from killing the runtime ([#61409](https://github.com/WordPress/gutenberg/pull/61409)) ## 5.6.0 (2024-05-02) @@ -20,8 +21,6 @@ - Hooks useMemo and useCallback should return a value. ([#60474](https://github.com/WordPress/gutenberg/pull/60474)) -- Prevent empty namespace or different namespaces from killing the runtime ([#61409](https://github.com/WordPress/gutenberg/pull/61409)) - ## 5.4.0 (2024-04-03) ## 5.3.0 (2024-03-21) From ac6d7b799535479075b713929b06e44d1d297834 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Tue, 7 May 2024 21:57:34 +0200 Subject: [PATCH 04/10] Update with isdebug, rephrase error --- packages/interactivity/src/directives.js | 7 ++----- packages/interactivity/src/hooks.tsx | 2 +- packages/interactivity/src/vdom.ts | 9 ++------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index c09cecb4afc12..b3ffd8d404e12 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -13,6 +13,7 @@ import { deepSignal, peek } from 'deepsignal'; import { useWatch, useInit } from './utils'; import { directive, getScope, getEvaluate } from './hooks'; import { kebabToCamelCase } from './utils/kebab-to-camelcase'; +import { isDebug } from './constants'; // Assigned objects should be ignore during proxification. const contextAssignedObjects = new WeakMap(); @@ -242,11 +243,7 @@ export default () => { if ( defaultEntry ) { const { namespace, value } = defaultEntry; // Check that the value is a JSON object. Send a console warning if not. - if ( - typeof SCRIPT_DEBUG !== 'undefined' && - SCRIPT_DEBUG === true && - ! isPlainObject( value ) - ) { + if ( isDebug && ! isPlainObject( value ) ) { // eslint-disable-next-line no-console console.warn( `The value of data-wp-context in "${ namespace }" store must be a valid stringified JSON object.` diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 76356f04d886e..4463a455ac81f 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -286,7 +286,7 @@ const resolve = ( path, namespace ) => { if ( isDebug ) { // eslint-disable-next-line no-console console.warn( - `The namespace "${ namespace }" defined in "data-wp-interactive" does not match with the store.` + `There was an error when trying to resolve the path "${ path }" in the namespace "${ namespace }".` ); } } diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 9e6221bb871f4..dcd4bde60fb41 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -5,7 +5,7 @@ import { h } from 'preact'; /** * Internal dependencies */ -import { directivePrefix as p } from './constants'; +import { isDebug, directivePrefix as p } from './constants'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; @@ -120,12 +120,7 @@ export function toVdom( root ) { ( obj, [ name, ns, value ] ) => { const directiveMatch = directiveParser.exec( name ); if ( directiveMatch === null ) { - if ( - // @ts-expect-error This is a debug-only warning. - typeof SCRIPT_DEBUG !== 'undefined' && - // @ts-expect-error This is a debug-only warning. - SCRIPT_DEBUG === true - ) { + if ( isDebug ) { // eslint-disable-next-line no-console console.warn( `Invalid directive: ${ name }.` ); } From 6491f6be0a9c2b739c1ae1126d5e46c1a0203095 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 9 May 2024 17:27:11 +0200 Subject: [PATCH 05/10] Prevent warning duplication --- packages/interactivity/src/constants.js | 3 --- packages/interactivity/src/directives.js | 7 +++---- packages/interactivity/src/hooks.tsx | 26 ++++++++---------------- packages/interactivity/src/utils/warn.ts | 21 +++++++++++++++++++ packages/interactivity/src/vdom.ts | 8 +++----- 5 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 packages/interactivity/src/utils/warn.ts diff --git a/packages/interactivity/src/constants.js b/packages/interactivity/src/constants.js index aecf919067bdb..669e94263fb9c 100644 --- a/packages/interactivity/src/constants.js +++ b/packages/interactivity/src/constants.js @@ -1,4 +1 @@ export const directivePrefix = 'wp'; - -export const isDebug = - typeof SCRIPT_DEBUG !== 'undefined' && SCRIPT_DEBUG === true; diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index b3ffd8d404e12..8100ac7a52eee 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -13,7 +13,7 @@ import { deepSignal, peek } from 'deepsignal'; import { useWatch, useInit } from './utils'; import { directive, getScope, getEvaluate } from './hooks'; import { kebabToCamelCase } from './utils/kebab-to-camelcase'; -import { isDebug } from './constants'; +import { warn } from './utils/warn'; // Assigned objects should be ignore during proxification. const contextAssignedObjects = new WeakMap(); @@ -243,9 +243,8 @@ export default () => { if ( defaultEntry ) { const { namespace, value } = defaultEntry; // Check that the value is a JSON object. Send a console warning if not. - if ( isDebug && ! isPlainObject( value ) ) { - // eslint-disable-next-line no-console - console.warn( + if ( ! isPlainObject( value ) ) { + warn( `The value of data-wp-context in "${ namespace }" store must be a valid stringified JSON object.` ); } diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 4463a455ac81f..7100fab615c14 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -16,7 +16,7 @@ import type { VNode, Context, RefObject } from 'preact'; * Internal dependencies */ import { store, stores, universalUnlock } from './store'; -import { isDebug } from './constants'; +import { warn } from './utils/warn'; interface DirectiveEntry { value: string | Object; namespace: string; @@ -261,15 +261,6 @@ export const directive = ( // Resolve the path to some property of the store object. const resolve = ( path, namespace ) => { - if ( namespace === '' ) { - if ( isDebug ) { - // eslint-disable-next-line no-console - console.warn( - `Namespace cannot be an empty string. Error found when trying to use "${ path }"` - ); - } - return; - } let resolvedStore = stores.get( namespace ); if ( typeof resolvedStore === 'undefined' ) { resolvedStore = store( namespace, undefined, { @@ -282,14 +273,7 @@ const resolve = ( path, namespace ) => { }; try { return path.split( '.' ).reduce( ( acc, key ) => acc[ key ], current ); - } catch ( e ) { - if ( isDebug ) { - // eslint-disable-next-line no-console - console.warn( - `There was an error when trying to resolve the path "${ path }" in the namespace "${ namespace }".` - ); - } - } + } catch ( e ) {} }; // Generate the evaluate function. @@ -300,6 +284,12 @@ export const getEvaluate: GetEvaluate = if ( typeof path !== 'string' ) { throw new Error( 'The `value` prop should be a string path' ); } + if ( ! namespace || namespace === '' ) { + // TODO: Support lazy/dynamically initialized stores + warn( + `The "namespace" cannot be "{}", "null" or an emtpy string. Path: ${ path }` + ); + } // If path starts with !, remove it and save a flag. const hasNegationOperator = path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); diff --git a/packages/interactivity/src/utils/warn.ts b/packages/interactivity/src/utils/warn.ts new file mode 100644 index 0000000000000..98bf88c157aca --- /dev/null +++ b/packages/interactivity/src/utils/warn.ts @@ -0,0 +1,21 @@ +const logged = new Set(); + +export const warn = ( message ) => { + // @ts-expect-error + if ( typeof SCRIPT_DEBUG !== 'undefined' && SCRIPT_DEBUG === true ) { + if ( logged.has( message ) ) { + return; + } + + // eslint-disable-next-line no-console + console.warn( message ); + + // Adding a stack trace to the warning message to help with debugging. + try { + throw Error( message ); + } catch ( e ) { + // Do nothing. + } + logged.add( message ); + } +}; diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index dcd4bde60fb41..78f6d7032613a 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -5,7 +5,8 @@ import { h } from 'preact'; /** * Internal dependencies */ -import { isDebug, directivePrefix as p } from './constants'; +import { directivePrefix as p } from './constants'; +import { warn } from './utils/warn'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; @@ -120,10 +121,7 @@ export function toVdom( root ) { ( obj, [ name, ns, value ] ) => { const directiveMatch = directiveParser.exec( name ); if ( directiveMatch === null ) { - if ( isDebug ) { - // eslint-disable-next-line no-console - console.warn( `Invalid directive: ${ name }.` ); - } + warn( `Invalid directive: ${ name }.` ); return obj; } const prefix = directiveMatch[ 1 ] || ''; From 164006448235ea1650b188e98abc1ffd88ee5c22 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 9 May 2024 17:59:32 +0200 Subject: [PATCH 06/10] Remove not needed check --- packages/interactivity/src/hooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 7100fab615c14..377e62064ef10 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -284,7 +284,7 @@ export const getEvaluate: GetEvaluate = if ( typeof path !== 'string' ) { throw new Error( 'The `value` prop should be a string path' ); } - if ( ! namespace || namespace === '' ) { + if ( ! namespace ) { // TODO: Support lazy/dynamically initialized stores warn( `The "namespace" cannot be "{}", "null" or an emtpy string. Path: ${ path }` From 605eeac232b1865020ef6a49870cce891a44007f Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 9 May 2024 18:13:15 +0200 Subject: [PATCH 07/10] Move warn to same resolve function --- packages/interactivity/src/hooks.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 377e62064ef10..946919de7a953 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -261,6 +261,12 @@ export const directive = ( // Resolve the path to some property of the store object. const resolve = ( path, namespace ) => { + if ( ! namespace ) { + warn( + `The "namespace" cannot be "{}", "null" or an emtpy string. Path: ${ path }` + ); + return; + } let resolvedStore = stores.get( namespace ); if ( typeof resolvedStore === 'undefined' ) { resolvedStore = store( namespace, undefined, { @@ -272,8 +278,13 @@ const resolve = ( path, namespace ) => { context: getScope().context[ namespace ], }; try { + // TODO: Support lazy/dynamically initialized stores return path.split( '.' ).reduce( ( acc, key ) => acc[ key ], current ); - } catch ( e ) {} + } catch ( e ) { + warn( + `The path "${ path }" could not be resolved in the "${ namespace }" store.` + ); + } }; // Generate the evaluate function. @@ -284,12 +295,6 @@ export const getEvaluate: GetEvaluate = if ( typeof path !== 'string' ) { throw new Error( 'The `value` prop should be a string path' ); } - if ( ! namespace ) { - // TODO: Support lazy/dynamically initialized stores - warn( - `The "namespace" cannot be "{}", "null" or an emtpy string. Path: ${ path }` - ); - } // If path starts with !, remove it and save a flag. const hasNegationOperator = path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); From 414e15b820f0a86ab10ba3c8eac822447735d279 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Thu, 9 May 2024 18:35:53 +0200 Subject: [PATCH 08/10] Add some tests --- .../interactive-blocks/namespace/block.json | 15 ++++++ .../interactive-blocks/namespace/render.php | 23 +++++++++ .../namespace/view.asset.php | 1 + .../interactive-blocks/namespace/view.js | 16 ++++++ .../e2e/specs/interactivity/namespace.spec.ts | 49 +++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 packages/e2e-tests/plugins/interactive-blocks/namespace/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/namespace/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/namespace/view.asset.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/namespace/view.js create mode 100644 test/e2e/specs/interactivity/namespace.spec.ts diff --git a/packages/e2e-tests/plugins/interactive-blocks/namespace/block.json b/packages/e2e-tests/plugins/interactive-blocks/namespace/block.json new file mode 100644 index 0000000000000..c0382286a1621 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/namespace/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test-namespace/directive-bind", + "title": "E2E Interactivity tests - directive bind", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScriptModule": "file:./view.js", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/namespace/render.php b/packages/e2e-tests/plugins/interactive-blocks/namespace/render.php new file mode 100644 index 0000000000000..6fdc2d07e350c --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/namespace/render.php @@ -0,0 +1,23 @@ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/packages/e2e-tests/plugins/interactive-blocks/namespace/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/namespace/view.asset.php new file mode 100644 index 0000000000000..db23afdf657a1 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/namespace/view.asset.php @@ -0,0 +1 @@ + array( '@wordpress/interactivity' ) ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/namespace/view.js b/packages/e2e-tests/plugins/interactive-blocks/namespace/view.js new file mode 100644 index 0000000000000..9225f88ce9d27 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/namespace/view.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { store } from '@wordpress/interactivity'; + +store( 'namespace', { + state: { + url: '/some-url', + }, +} ); + +store( 'other', { + state: { + url: '/other-store-url', + }, +} ); diff --git a/test/e2e/specs/interactivity/namespace.spec.ts b/test/e2e/specs/interactivity/namespace.spec.ts new file mode 100644 index 0000000000000..fd38a7ecf6ed6 --- /dev/null +++ b/test/e2e/specs/interactivity/namespace.spec.ts @@ -0,0 +1,49 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'Namespaces', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + await utils.activatePlugins(); + await utils.addPostWithBlock( 'test-namespace/directive-bind' ); + } ); + + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'test-namespace/directive-bind' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'Empty string as namespace should not work', async ( { page } ) => { + const el = page.getByTestId( 'empty namespace' ); + await expect( el ).not.toHaveAttribute( 'href', '/some-url' ); + } ); + + test( 'A string as namespace should work', async ( { page } ) => { + const el = page.getByTestId( 'correct namespace' ); + await expect( el ).toHaveAttribute( 'href', '/some-url' ); + } ); + + test( 'An empty object as namespace should work', async ( { page } ) => { + const el = page.getByTestId( 'object namespace' ); + await expect( el ).not.toHaveAttribute( 'href', '/some-url' ); + } ); + + test( 'A wrong namespace should not break the runtime', async ( { + page, + } ) => { + const el = page.getByTestId( 'object namespace' ); + await expect( el ).not.toHaveAttribute( 'href', '/some-url' ); + const correct = page.getByTestId( 'correct namespace' ); + await expect( correct ).toHaveAttribute( 'href', '/some-url' ); + } ); + + test( 'A different store namespace should work', async ( { page } ) => { + const el = page.getByTestId( 'other namespace' ); + await expect( el ).toHaveAttribute( 'href', '/other-store-url' ); + } ); +} ); From f587e3d735e400b03bf981504d9a6c9ada8af554 Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Mon, 13 May 2024 10:53:25 +0200 Subject: [PATCH 09/10] Remove not needed warn --- packages/interactivity/src/hooks.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 946919de7a953..b224647cd9096 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -280,11 +280,7 @@ const resolve = ( path, namespace ) => { try { // TODO: Support lazy/dynamically initialized stores return path.split( '.' ).reduce( ( acc, key ) => acc[ key ], current ); - } catch ( e ) { - warn( - `The path "${ path }" could not be resolved in the "${ namespace }" store.` - ); - } + } catch ( e ) {} }; // Generate the evaluate function. From 3679c6b18538b1c57ef1b960f9c695368d5ecbee Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Tue, 14 May 2024 14:56:04 +0200 Subject: [PATCH 10/10] Fix typos --- packages/block-library/src/embed/util.js | 2 +- packages/interactivity/src/hooks.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index 528dec45d1ecf..2af0e6adbfc6f 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -177,7 +177,7 @@ export const removeAspectRatioClasses = ( existingClassNames ) => { if ( ! existingClassNames ) { // Avoids extraneous work and also, by returning the same value as // received, ensures the post is not dirtied by a change of the block - // attribute from `undefined` to an emtpy string. + // attribute from `undefined` to an empty string. return existingClassNames; } const aspectRatioClassNames = ASPECT_RATIOS.reduce( diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index b224647cd9096..353959ea5b2ea 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -263,7 +263,7 @@ export const directive = ( const resolve = ( path, namespace ) => { if ( ! namespace ) { warn( - `The "namespace" cannot be "{}", "null" or an emtpy string. Path: ${ path }` + `The "namespace" cannot be "{}", "null" or an empty string. Path: ${ path }` ); return; }