From c38ae94df9b6333390c5b290d03c592515ae475a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 14 Mar 2024 11:33:32 +0100 Subject: [PATCH 01/23] Strict type checking --- packages/interactivity/tsconfig.json | 11 +++++++++-- tsconfig.base.json | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index 0fb91956773f89..445fce42e73e54 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -4,8 +4,15 @@ "compilerOptions": { "rootDir": "src", "declarationDir": "build-types", - "checkJs": false, - "strict": false + + "alwaysStrict": false, + "strictNullChecks": false, + "strictBindCallApply": false, + "strictFunctionTypes": false, + "strictPropertyInitialization": false, + "noImplicitAny": false, + "noImplicitThis": false, + "useUnknownInCatchVariables": false }, "include": [ "src/**/*" ] } diff --git a/tsconfig.base.json b/tsconfig.base.json index 1b30522ee52366..18388b7f71854c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -7,7 +7,7 @@ "jsx": "preserve", "target": "esnext", "module": "esnext", - "lib": [ "dom", "esnext" ], + "lib": [ "DOM", "DOM.Iterable", "ESNext" ], "declaration": true, "declarationMap": true, "composite": true, From 613531cc499404a1422f904098a797acf75ca0b7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 16:09:26 +0200 Subject: [PATCH 02/23] rename to tsx --- packages/interactivity/src/{directives.js => directives.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/interactivity/src/{directives.js => directives.tsx} (100%) diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.tsx similarity index 100% rename from packages/interactivity/src/directives.js rename to packages/interactivity/src/directives.tsx From d65fc7a9a360c65289db65fb4222c0fb2fafa40d Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 17:01:54 +0200 Subject: [PATCH 03/23] isPlainObject --- packages/interactivity/src/directives.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index 8100ac7a52eee5..67fe9d92f2714a 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -23,8 +23,8 @@ const contextObjectToProxy = new WeakMap(); const contextProxyToObject = new WeakMap(); const contextObjectToFallback = new WeakMap(); -const isPlainObject = ( item ) => - item && typeof item === 'object' && item.constructor === Object; +const isPlainObject = ( item: unknown ): boolean => + Boolean( item && typeof item === 'object' && item.constructor === Object ); const descriptor = Reflect.getOwnPropertyDescriptor; From 89a5d8e3c1f14ebe3d3e6eee6993aed382f16c8c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 17:05:34 +0200 Subject: [PATCH 04/23] hooks types --- packages/interactivity/src/hooks.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 353959ea5b2ea6..be90889dc69952 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -1,5 +1,8 @@ /* @jsx createElement */ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable react-hooks/exhaustive-deps */ + /** * External dependencies */ @@ -18,7 +21,7 @@ import type { VNode, Context, RefObject } from 'preact'; import { store, stores, universalUnlock } from './store'; import { warn } from './utils/warn'; interface DirectiveEntry { - value: string | Object; + value: string | object; namespace: string; suffix: string; } @@ -102,7 +105,7 @@ const immutableError = () => { 'Please use `data-wp-bind` to modify the attributes of an element.' ); }; -const immutableHandlers = { +const immutableHandlers: ProxyHandler< object > = { get( target, key, receiver ) { const value = Reflect.get( target, key, receiver ); return !! value && typeof value === 'object' @@ -112,7 +115,7 @@ const immutableHandlers = { set: immutableError, deleteProperty: immutableError, }; -const deepImmutable = < T extends Object = {} >( target: T ): T => { +const deepImmutable = < T extends object = {} >( target: T ): T => { if ( ! immutableMap.has( target ) ) { immutableMap.set( target, new Proxy( target, immutableHandlers ) ); } @@ -260,7 +263,7 @@ export const directive = ( }; // Resolve the path to some property of the store object. -const resolve = ( path, namespace ) => { +const resolve = ( path: string, namespace: string ) => { if ( ! namespace ) { warn( `The "namespace" cannot be "{}", "null" or an empty string. Path: ${ path }` From 6b5002f7a279f2738d8bdb19a3836651a7e6b295 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 17:47:58 +0200 Subject: [PATCH 05/23] types --- packages/interactivity/src/directives.tsx | 65 +++++++++++++---------- packages/interactivity/src/hooks.tsx | 10 ++-- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index 67fe9d92f2714a..a1cae36e63d262 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -1,9 +1,12 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable react-hooks/exhaustive-deps */ + /* @jsx createElement */ /** * External dependencies */ -import { h as createElement } from 'preact'; +import { h as createElement, type RefObject } from 'preact'; import { useContext, useMemo, useRef } from 'preact/hooks'; import { deepSignal, peek } from 'deepsignal'; @@ -37,12 +40,12 @@ const descriptor = Reflect.getOwnPropertyDescriptor; * By default, all plain objects inside the context are wrapped, unless it is * listed in the `ignore` option. * - * @param {Object} current Current context. - * @param {Object} inherited Inherited context, used as fallback. + * @param current Current context. + * @param inherited Inherited context, used as fallback. * - * @return {Object} The wrapped context object. + * @return The wrapped context object. */ -const proxifyContext = ( current, inherited = {} ) => { +const proxifyContext = ( current: object, inherited: object = {} ): object => { // Update the fallback object reference when it changes. contextObjectToFallback.set( current, inherited ); if ( ! contextObjectToProxy.has( current ) ) { @@ -127,10 +130,10 @@ const proxifyContext = ( current, inherited = {} ) => { /** * Recursively update values within a deepSignal object. * - * @param {Object} target A deepSignal instance. - * @param {Object} source Object with properties to update in `target` + * @param target A deepSignal instance. + * @param source Object with properties to update in `target` */ -const updateSignals = ( target, source ) => { +const updateSignals = ( target: object, source: object ) => { for ( const k in source ) { if ( isPlainObject( peek( target, k ) ) && @@ -146,23 +149,23 @@ const updateSignals = ( target, source ) => { /** * Recursively clone the passed object. * - * @param {Object} source Source object. - * @return {Object} Cloned object. + * @param source Source object. + * @return Cloned object. */ -const deepClone = ( source ) => { +function deepClone< T >( source: T ): T { if ( isPlainObject( source ) ) { return Object.fromEntries( - Object.entries( source ).map( ( [ key, value ] ) => [ + Object.entries( source as object ).map( ( [ key, value ] ) => [ key, deepClone( value ), ] ) - ); + ) as T; } if ( Array.isArray( source ) ) { - return source.map( ( i ) => deepClone( i ) ); + return source.map( ( i ) => deepClone( i ) ) as T; } return source; -}; +} const newRule = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g; @@ -176,10 +179,10 @@ const empty = ' '; * Made by Cristian Bote (@cristianbote) for Goober. * https://unpkg.com/browse/goober@2.1.13/src/core/astish.js * - * @param {string} val CSS string. - * @return {Object} CSS object. + * @param val CSS string. + * @return CSS object. */ -const cssStringToObject = ( val ) => { +const cssStringToObject = ( val: string ): object => { const tree = [ {} ]; let block, left; @@ -203,10 +206,9 @@ const cssStringToObject = ( val ) => { * Creates a directive that adds an event listener to the global window or * document object. * - * @param {string} type 'window' or 'document' - * @return {void} + * @param type 'window' or 'document' */ -const getGlobalEventDirective = ( type ) => { +const getGlobalEventDirective = ( type: 'window' | 'document' ) => { return ( { directives, evaluate } ) => { directives[ `on-${ type }` ] .filter( ( { suffix } ) => suffix !== 'default' ) @@ -217,7 +219,7 @@ const getGlobalEventDirective = ( type ) => { const globalVar = type === 'window' ? window : document; globalVar.addEventListener( eventName, cb ); return () => globalVar.removeEventListener( eventName, cb ); - }, [] ); + } ); } ); }; }; @@ -333,9 +335,13 @@ export default () => { * need deps because it only needs to do it the first time. */ if ( ! result ) { - element.ref.current.classList.remove( className ); + ( + element.ref as RefObject< HTMLElement > + ).current!.classList.remove( className ); } else { - element.ref.current.classList.add( className ); + ( + element.ref as RefObject< HTMLElement > + ).current!.classList.add( className ); } } ); } ); @@ -368,9 +374,13 @@ export default () => { * because it only needs to do it the first time. */ if ( ! result ) { - element.ref.current.style.removeProperty( styleProp ); + ( + element.ref as RefObject< HTMLElement > + ).current!.style.removeProperty( styleProp ); } else { - element.ref.current.style[ styleProp ] = result; + ( + element.ref as RefObject< HTMLElement > + ).current!.style[ styleProp ] = result; } } ); } ); @@ -390,7 +400,8 @@ export default () => { * first time. After that, Preact will handle the changes. */ useInit( () => { - const el = element.ref.current; + const el = ( element.ref as RefObject< HTMLElement > ) + .current!; /* * We set the value directly to the corresponding HTMLElement instance diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index be90889dc69952..655118726f79b5 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -11,6 +11,7 @@ import { options, createContext, cloneElement, + type ComponentChildren, } from 'preact'; import { useRef, useCallback, useContext } from 'preact/hooks'; import type { VNode, Context, RefObject } from 'preact'; @@ -36,11 +37,14 @@ interface DirectiveArgs { /** * Props present in the current element. */ - props: Object; + props: { children?: ComponentChildren }; /** * Virtual node representing the element. */ - element: VNode; + element: VNode< { + class?: string; + style?: string | Record< string, string | number >; + } >; /** * The inherited context. */ @@ -53,7 +57,7 @@ interface DirectiveArgs { } interface DirectiveCallback { - ( args: DirectiveArgs ): VNode | void; + ( args: DirectiveArgs ): VNode | null; } interface DirectiveOptions { From 78c697367ce05f7dfde9f7bf1e3c48ffcb9a7c17 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 18:11:32 +0200 Subject: [PATCH 06/23] types --- packages/interactivity/src/directives.tsx | 17 ++++++++++++----- packages/interactivity/src/hooks.tsx | 5 +++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index a1cae36e63d262..1b94c3fc5d26ff 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -8,7 +8,7 @@ */ import { h as createElement, type RefObject } from 'preact'; import { useContext, useMemo, useRef } from 'preact/hooks'; -import { deepSignal, peek } from 'deepsignal'; +import { deepSignal, peek, type DeepSignal } from 'deepsignal'; /** * Internal dependencies @@ -50,7 +50,7 @@ const proxifyContext = ( current: object, inherited: object = {} ): object => { contextObjectToFallback.set( current, inherited ); if ( ! contextObjectToProxy.has( current ) ) { const proxy = new Proxy( current, { - get: ( target, k ) => { + get: ( target: DeepSignal< any >, k ) => { const fallback = contextObjectToFallback.get( current ); // Always subscribe to prop changes in the current context. const currentProp = target[ k ]; @@ -131,9 +131,12 @@ const proxifyContext = ( current: object, inherited: object = {} ): object => { * Recursively update values within a deepSignal object. * * @param target A deepSignal instance. - * @param source Object with properties to update in `target` + * @param source Object with properties to update in `target`. */ -const updateSignals = ( target: object, source: object ) => { +const updateSignals = ( + target: DeepSignal< any >, + source: DeepSignal< any > +) => { for ( const k in source ) { if ( isPlainObject( peek( target, k ) ) && @@ -182,7 +185,9 @@ const empty = ' '; * @param val CSS string. * @return CSS object. */ -const cssStringToObject = ( val: string ): object => { +const cssStringToObject = ( + val: string +): Record< string, string | number > => { const tree = [ {} ]; let block, left; @@ -473,6 +478,8 @@ export default () => { type: Type, props: { innerHTML, ...rest }, }, + }: { + element: any; } ) => { // Preserve the initial inner HTML. const cached = useMemo( () => innerHTML, [] ); diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 655118726f79b5..4fac736aeadafb 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -44,6 +44,7 @@ interface DirectiveArgs { element: VNode< { class?: string; style?: string | Record< string, string | number >; + content?: ComponentChildren; } >; /** * The inherited context. @@ -57,7 +58,7 @@ interface DirectiveArgs { } interface DirectiveCallback { - ( args: DirectiveArgs ): VNode | null; + ( args: DirectiveArgs ): VNode | null | void; } interface DirectiveOptions { @@ -72,7 +73,7 @@ interface DirectiveOptions { interface Scope { evaluate: Evaluate; - context: Context< any >; + context?: object; ref: RefObject< HTMLElement >; attributes: createElement.JSX.HTMLAttributes; } From a64715f133efeadf6de71407669e1d280c33b2ec Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 18:13:14 +0200 Subject: [PATCH 07/23] =?UTF-8?q?stricter=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/interactivity/tsconfig.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index 445fce42e73e54..ef79d9873e1989 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -5,14 +5,11 @@ "rootDir": "src", "declarationDir": "build-types", - "alwaysStrict": false, "strictNullChecks": false, - "strictBindCallApply": false, - "strictFunctionTypes": false, "strictPropertyInitialization": false, - "noImplicitAny": false, + "noImplicitThis": false, - "useUnknownInCatchVariables": false + "noImplicitAny": false }, "include": [ "src/**/*" ] } From 2cbf09c5ffe0c195ba084d9ce47e40f31ca2e262 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 18:15:43 +0200 Subject: [PATCH 08/23] implicit this --- packages/interactivity/src/utils.ts | 2 +- packages/interactivity/tsconfig.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/interactivity/src/utils.ts b/packages/interactivity/src/utils.ts index 4759a7b27a74a6..c95b2421188efd 100644 --- a/packages/interactivity/src/utils.ts +++ b/packages/interactivity/src/utils.ts @@ -63,7 +63,7 @@ const afterNextFrame = ( callback: () => void ) => { */ function createFlusher( compute: () => unknown, notify: () => void ): Flusher { let flush: () => void; - const dispose = effect( function () { + const dispose = effect( function ( this: any ) { flush = this.c.bind( this ); this.x = compute; this.c = notify; diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index ef79d9873e1989..d991e2e56a9084 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -8,7 +8,6 @@ "strictNullChecks": false, "strictPropertyInitialization": false, - "noImplicitThis": false, "noImplicitAny": false }, "include": [ "src/**/*" ] From 3cacdc16add765c2f843e7bc7ef3fb8a2797fa8b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:17:42 +0200 Subject: [PATCH 09/23] strict nulls --- packages/interactivity/src/vdom.ts | 58 ++++++++++++++++++---------- packages/interactivity/tsconfig.json | 1 - 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 78f6d7032613a5..77458a9d6cc182 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -1,17 +1,19 @@ /** * External dependencies */ -import { h } from 'preact'; +import { h, type VNode, type JSX } from 'preact'; /** * Internal dependencies */ import { directivePrefix as p } from './constants'; import { warn } from './utils/warn'; +type TreeWalkerReturn = string | Node | VNode< any > | null; + const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; -const namespaces = []; +const namespaces: Array< string > = []; const currentNamespace = () => namespaces[ namespaces.length - 1 ] ?? null; // Regular expression for directive parsing. @@ -39,35 +41,49 @@ export const hydratedIslands = new WeakSet(); /** * Recursive function that transforms a DOM tree into vDOM. * - * @param {Node} root The root element or node to start traversing on. - * @return {import('preact').VNode[]} The resulting vDOM tree. + * @param root The root element or node to start traversing on. + * @return The resulting vDOM tree. */ -export function toVdom( root ) { +export function toVdom( root: Node ): [ string | VNode | null, Node | null ] { const treeWalker = document.createTreeWalker( root, 205 // ELEMENT + TEXT + COMMENT + CDATA_SECTION + PROCESSING_INSTRUCTION ); - function walk( node ) { - const { attributes, nodeType, localName } = node; + function walk( node: Node ): [ string | VNode | null, Node | null ] { + const { nodeType } = node; + // TEXT_NODE (3) if ( nodeType === 3 ) { - return [ node.data ]; + return [ ( node as Text ).data, null ]; } + + // CDATA_SECTION_NODE (4) if ( nodeType === 4 ) { const next = treeWalker.nextSibling(); - node.replaceWith( new window.Text( node.nodeValue ) ); + ( node as CDATASection ).replaceWith( + new window.Text( ( node as CDATASection ).nodeValue ?? '' ) + ); return [ node.nodeValue, next ]; } + + // COMMENT_NODE (8) || PROCESSING_INSTRUCTION_NODE (7) if ( nodeType === 8 || nodeType === 7 ) { const next = treeWalker.nextSibling(); - node.remove(); + ( node as Comment | ProcessingInstruction ).remove(); return [ null, next ]; } + const elementNode = node as HTMLElement; + + const attributes = elementNode.attributes; + const localName = elementNode.localName as keyof JSX.IntrinsicElements; + const props: Record< string, any > = {}; - const children = []; - const directives = []; + const children: Array< TreeWalkerReturn > = []; + const directives: Array< + [ name: string, namespace: string | null, value: unknown ] + > = []; let ignore = false; let island = false; @@ -84,7 +100,7 @@ export function toVdom( root ) { .exec( attributes[ i ].value ) ?.slice( 1 ) ?? [ null, attributes[ i ].value ]; try { - value = JSON.parse( value ); + value = JSON.parse( value as string ); } catch ( e ) {} if ( n === islandAttr ) { island = true; @@ -105,15 +121,16 @@ export function toVdom( root ) { if ( ignore && ! island ) { return [ - h( localName, { + h< any, any >( localName, { ...props, - innerHTML: node.innerHTML, + innerHTML: elementNode.innerHTML, __directives: { ignore: true }, } ), + null, ]; } if ( island ) { - hydratedIslands.add( node ); + hydratedIslands.add( elementNode ); } if ( directives.length ) { @@ -139,10 +156,11 @@ export function toVdom( root ) { ); } + // @ts-expect-error Fixed in upcoming preact release https://github.com/preactjs/preact/pull/4334 if ( localName === 'template' ) { - props.content = [ ...node.content.childNodes ].map( ( childNode ) => - toVdom( childNode ) - ); + props.content = [ + ...( elementNode as HTMLTemplateElement ).content.childNodes, + ].map( ( childNode ) => toVdom( childNode ) ); } else { let child = treeWalker.firstChild(); if ( child ) { @@ -162,7 +180,7 @@ export function toVdom( root ) { namespaces.pop(); } - return [ h( localName, props, children ) ]; + return [ h( localName, props, children ), null ]; } return walk( treeWalker.currentNode ); diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index d991e2e56a9084..bd921d194888ac 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -5,7 +5,6 @@ "rootDir": "src", "declarationDir": "build-types", - "strictNullChecks": false, "strictPropertyInitialization": false, "noImplicitAny": false From ff261d457621b86147b5845593a2da6b5a56937a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:20:38 +0200 Subject: [PATCH 10/23] handle not found text entry --- packages/interactivity/src/directives.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index 1b94c3fc5d26ff..07328df8af210b 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -495,6 +495,11 @@ export default () => { // data-wp-text directive( 'text', ( { directives: { text }, element, evaluate } ) => { const entry = text.find( ( { suffix } ) => suffix === 'default' ); + if ( ! entry ) { + element.props.children = null; + return; + } + try { const result = evaluate( entry ); element.props.children = From 646ea331422be7bf4c02589ab5e56b7755980ea2 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:25:57 +0200 Subject: [PATCH 11/23] hooks --- packages/interactivity/src/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/interactivity/src/utils.ts b/packages/interactivity/src/utils.ts index c95b2421188efd..e201ff15a68927 100644 --- a/packages/interactivity/src/utils.ts +++ b/packages/interactivity/src/utils.ts @@ -62,7 +62,7 @@ const afterNextFrame = ( callback: () => void ) => { * @return The Flusher object with `flush` and `dispose` properties. */ function createFlusher( compute: () => unknown, notify: () => void ): Flusher { - let flush: () => void; + let flush: () => void = () => undefined; const dispose = effect( function ( this: any ) { flush = this.c.bind( this ); this.x = compute; @@ -82,7 +82,7 @@ function createFlusher( compute: () => unknown, notify: () => void ): Flusher { */ export function useSignalEffect( callback: () => unknown ) { _useEffect( () => { - let eff = null; + let eff: Flusher | null = null; let isExecuting = false; const notify = async () => { @@ -273,7 +273,7 @@ export const createRootFragment = ( parent: Element, replaceNode: Node | Node[] ) => { - replaceNode = [].concat( replaceNode ); + replaceNode = ( [] as Node[] ).concat( replaceNode ); const sibling = replaceNode[ replaceNode.length - 1 ].nextSibling; function insert( child: any, root: any ) { parent.insertBefore( child, root || sibling ); From 7739b7498714a9bba58899bebfe5f0484c7bd9ca Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:27:11 +0200 Subject: [PATCH 12/23] store --- packages/interactivity/src/store.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index 81905071702af2..87c9333c744296 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -17,7 +17,7 @@ import { } from './hooks'; const isObject = ( item: unknown ): item is Record< string, unknown > => - item && typeof item === 'object' && item.constructor === Object; + Boolean( item && typeof item === 'object' && item.constructor === Object ); const deepMerge = ( target: any, source: any ) => { if ( isObject( target ) && isObject( source ) ) { @@ -338,12 +338,12 @@ export const populateInitialData = ( data?: { config?: Record< string, unknown >; } ) => { if ( isObject( data?.state ) ) { - Object.entries( data.state ).forEach( ( [ namespace, state ] ) => { + Object.entries( data!.state ).forEach( ( [ namespace, state ] ) => { store( namespace, { state }, { lock: universalUnlock } ); } ); } if ( isObject( data?.config ) ) { - Object.entries( data.config ).forEach( ( [ namespace, config ] ) => { + Object.entries( data!.config ).forEach( ( [ namespace, config ] ) => { storeConfigs.set( namespace, config ); } ); } From ec9f2e0ad8170d61c660456b0c2301411cd272cf Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:29:49 +0200 Subject: [PATCH 13/23] Scope --- 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 4fac736aeadafb..2dfc08a43f6fa8 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -73,7 +73,7 @@ interface DirectiveOptions { interface Scope { evaluate: Evaluate; - context?: object; + context: object; ref: RefObject< HTMLElement >; attributes: createElement.JSX.HTMLAttributes; } From 85283eb3e5efb9723faf44fcf7b533ed151ecaa8 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:31:49 +0200 Subject: [PATCH 14/23] I hate it --- packages/interactivity/src/vdom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 77458a9d6cc182..894db31a5337d0 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -107,7 +107,7 @@ export function toVdom( root: Node ): [ string | VNode | null, Node | null ] { namespaces.push( typeof value === 'string' ? value - : value?.namespace ?? null + : ( value as any )?.namespace ?? null ); } else { directives.push( [ n, ns, value ] ); From 811043614daa9da9a93a140d322d9037cf7fb196 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:32:46 +0200 Subject: [PATCH 15/23] More strict --- packages/interactivity/tsconfig.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index bd921d194888ac..1d154e2089065d 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -5,8 +5,6 @@ "rootDir": "src", "declarationDir": "build-types", - "strictPropertyInitialization": false, - "noImplicitAny": false }, "include": [ "src/**/*" ] From 807e6ac500ea1104a0ecef73795c8b1f2c3c61c0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 15 May 2024 18:17:00 +0200 Subject: [PATCH 16/23] restart and try again --- packages/interactivity/src/vdom.ts | 60 +++++++++++------------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 894db31a5337d0..78f6d7032613a5 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -1,19 +1,17 @@ /** * External dependencies */ -import { h, type VNode, type JSX } from 'preact'; +import { h } from 'preact'; /** * Internal dependencies */ import { directivePrefix as p } from './constants'; import { warn } from './utils/warn'; -type TreeWalkerReturn = string | Node | VNode< any > | null; - const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; -const namespaces: Array< string > = []; +const namespaces = []; const currentNamespace = () => namespaces[ namespaces.length - 1 ] ?? null; // Regular expression for directive parsing. @@ -41,49 +39,35 @@ export const hydratedIslands = new WeakSet(); /** * Recursive function that transforms a DOM tree into vDOM. * - * @param root The root element or node to start traversing on. - * @return The resulting vDOM tree. + * @param {Node} root The root element or node to start traversing on. + * @return {import('preact').VNode[]} The resulting vDOM tree. */ -export function toVdom( root: Node ): [ string | VNode | null, Node | null ] { +export function toVdom( root ) { const treeWalker = document.createTreeWalker( root, 205 // ELEMENT + TEXT + COMMENT + CDATA_SECTION + PROCESSING_INSTRUCTION ); - function walk( node: Node ): [ string | VNode | null, Node | null ] { - const { nodeType } = node; + function walk( node ) { + const { attributes, nodeType, localName } = node; - // TEXT_NODE (3) if ( nodeType === 3 ) { - return [ ( node as Text ).data, null ]; + return [ node.data ]; } - - // CDATA_SECTION_NODE (4) if ( nodeType === 4 ) { const next = treeWalker.nextSibling(); - ( node as CDATASection ).replaceWith( - new window.Text( ( node as CDATASection ).nodeValue ?? '' ) - ); + node.replaceWith( new window.Text( node.nodeValue ) ); return [ node.nodeValue, next ]; } - - // COMMENT_NODE (8) || PROCESSING_INSTRUCTION_NODE (7) if ( nodeType === 8 || nodeType === 7 ) { const next = treeWalker.nextSibling(); - ( node as Comment | ProcessingInstruction ).remove(); + node.remove(); return [ null, next ]; } - const elementNode = node as HTMLElement; - - const attributes = elementNode.attributes; - const localName = elementNode.localName as keyof JSX.IntrinsicElements; - const props: Record< string, any > = {}; - const children: Array< TreeWalkerReturn > = []; - const directives: Array< - [ name: string, namespace: string | null, value: unknown ] - > = []; + const children = []; + const directives = []; let ignore = false; let island = false; @@ -100,14 +84,14 @@ export function toVdom( root: Node ): [ string | VNode | null, Node | null ] { .exec( attributes[ i ].value ) ?.slice( 1 ) ?? [ null, attributes[ i ].value ]; try { - value = JSON.parse( value as string ); + value = JSON.parse( value ); } catch ( e ) {} if ( n === islandAttr ) { island = true; namespaces.push( typeof value === 'string' ? value - : ( value as any )?.namespace ?? null + : value?.namespace ?? null ); } else { directives.push( [ n, ns, value ] ); @@ -121,16 +105,15 @@ export function toVdom( root: Node ): [ string | VNode | null, Node | null ] { if ( ignore && ! island ) { return [ - h< any, any >( localName, { + h( localName, { ...props, - innerHTML: elementNode.innerHTML, + innerHTML: node.innerHTML, __directives: { ignore: true }, } ), - null, ]; } if ( island ) { - hydratedIslands.add( elementNode ); + hydratedIslands.add( node ); } if ( directives.length ) { @@ -156,11 +139,10 @@ export function toVdom( root: Node ): [ string | VNode | null, Node | null ] { ); } - // @ts-expect-error Fixed in upcoming preact release https://github.com/preactjs/preact/pull/4334 if ( localName === 'template' ) { - props.content = [ - ...( elementNode as HTMLTemplateElement ).content.childNodes, - ].map( ( childNode ) => toVdom( childNode ) ); + props.content = [ ...node.content.childNodes ].map( ( childNode ) => + toVdom( childNode ) + ); } else { let child = treeWalker.firstChild(); if ( child ) { @@ -180,7 +162,7 @@ export function toVdom( root: Node ): [ string | VNode | null, Node | null ] { namespaces.pop(); } - return [ h( localName, props, children ), null ]; + return [ h( localName, props, children ) ]; } return walk( treeWalker.currentNode ); From 4d79587cc034d0764d8be8e0715ecab32708d71a Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 15 May 2024 18:42:42 +0200 Subject: [PATCH 17/23] Better --- packages/interactivity/src/vdom.ts | 86 ++++++++++++++++++------------ 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 78f6d7032613a5..86706e541b9561 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { h } from 'preact'; +import { h, type ComponentChild, type JSX } from 'preact'; /** * Internal dependencies */ @@ -11,7 +11,7 @@ import { warn } from './utils/warn'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; -const namespaces = []; +const namespaces: Array< string | null > = []; const currentNamespace = () => namespaces[ namespaces.length - 1 ] ?? null; // Regular expression for directive parsing. @@ -39,81 +39,100 @@ export const hydratedIslands = new WeakSet(); /** * Recursive function that transforms a DOM tree into vDOM. * - * @param {Node} root The root element or node to start traversing on. - * @return {import('preact').VNode[]} The resulting vDOM tree. + * @param root The root element or node to start traversing on. + * @return The resulting vDOM tree. */ -export function toVdom( root ) { +export function toVdom( root: Node ): Array< ComponentChild > { const treeWalker = document.createTreeWalker( root, - 205 // ELEMENT + TEXT + COMMENT + CDATA_SECTION + PROCESSING_INSTRUCTION + 205 // TEXT + CDATA_SECTION + COMMENT + PROCESSING_INSTRUCTION + ELEMENT ); - function walk( node ) { - const { attributes, nodeType, localName } = node; + function walk( + node: Node + ): [ ComponentChild ] | [ ComponentChild, Node | null ] { + const { nodeType } = node; + // TEXT_NODE (3) if ( nodeType === 3 ) { - return [ node.data ]; + return [ ( node as Text ).data ]; } + + // CDATA_SECTION_NODE (4) if ( nodeType === 4 ) { const next = treeWalker.nextSibling(); - node.replaceWith( new window.Text( node.nodeValue ) ); + ( node as CDATASection ).replaceWith( + new window.Text( ( node as CDATASection ).nodeValue ?? '' ) + ); return [ node.nodeValue, next ]; } + + // COMMENT_NODE (8) || PROCESSING_INSTRUCTION_NODE (7) if ( nodeType === 8 || nodeType === 7 ) { const next = treeWalker.nextSibling(); - node.remove(); + ( node as Comment | ProcessingInstruction ).remove(); return [ null, next ]; } + const elementNode = node as HTMLElement; + const { attributes } = elementNode; + const localName = elementNode.localName as keyof JSX.IntrinsicElements; + const props: Record< string, any > = {}; - const children = []; - const directives = []; + const children: Array< ComponentChild > = []; + const directives: Array< + [ name: string, namespace: string | null, value: unknown ] + > = []; let ignore = false; let island = false; for ( let i = 0; i < attributes.length; i++ ) { - const n = attributes[ i ].name; + const attributeName = attributes[ i ].name; if ( - n[ fullPrefix.length ] && - n.slice( 0, fullPrefix.length ) === fullPrefix + attributeName[ fullPrefix.length ] && + attributeName.slice( 0, fullPrefix.length ) === fullPrefix ) { - if ( n === ignoreAttr ) { + if ( attributeName === ignoreAttr ) { ignore = true; } else { - let [ ns, value ] = nsPathRegExp + const [ ns, value ] = nsPathRegExp .exec( attributes[ i ].value ) ?.slice( 1 ) ?? [ null, attributes[ i ].value ]; + let parsedValue: Record< string, unknown > | null = null; try { - value = JSON.parse( value ); + parsedValue = value ? JSON.parse( value ) : null; } catch ( e ) {} - if ( n === islandAttr ) { + if ( attributeName === islandAttr ) { island = true; - namespaces.push( - typeof value === 'string' + const namespace = + // eslint-disable-next-line no-nested-ternary + typeof parsedValue?.namespace === 'string' + ? parsedValue.namespace + : typeof value === 'string' ? value - : value?.namespace ?? null - ); + : null; + namespaces.push( namespace ); } else { - directives.push( [ n, ns, value ] ); + directives.push( [ attributeName, ns, value ] ); } } - } else if ( n === 'ref' ) { + } else if ( attributeName === 'ref' ) { continue; } - props[ n ] = attributes[ i ].value; + props[ attributeName ] = attributes[ i ].value; } if ( ignore && ! island ) { return [ - h( localName, { + h< any, any >( localName, { ...props, - innerHTML: node.innerHTML, + innerHTML: elementNode.innerHTML, __directives: { ignore: true }, } ), ]; } if ( island ) { - hydratedIslands.add( node ); + hydratedIslands.add( elementNode ); } if ( directives.length ) { @@ -139,10 +158,11 @@ export function toVdom( root ) { ); } + // @ts-expect-error Fixed in upcoming preact release https://github.com/preactjs/preact/pull/4334 if ( localName === 'template' ) { - props.content = [ ...node.content.childNodes ].map( ( childNode ) => - toVdom( childNode ) - ); + props.content = [ + ...( elementNode as HTMLTemplateElement ).content.childNodes, + ].map( ( childNode ) => toVdom( childNode ) ); } else { let child = treeWalker.firstChild(); if ( child ) { From 982606e2cb8b93378817d5a77587f3881966124e Mon Sep 17 00:00:00 2001 From: Carlos Bravo Date: Wed, 15 May 2024 18:53:43 +0200 Subject: [PATCH 18/23] Update changelog --- packages/interactivity/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index fc22086bef443a..4c54801c0210d5 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,10 +2,13 @@ ## Unreleased +### Enhancements + +- Strict type checking. ([#59865] (https://github.com/WordPress/gutenberg/pull/59865/)) + ### Bug Fixes - 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)) From dbf231aae35054d962a8aa5b8293f19130c95857 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 15 May 2024 19:32:56 +0200 Subject: [PATCH 19/23] Fix namespace regression, use named groups --- packages/interactivity/src/vdom.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 86706e541b9561..4933682749fb90 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -32,7 +32,7 @@ const directiveParser = new RegExp( // the reference, separated by `::`, like `some-namespace::state.somePath`. // Namespaces can contain any alphanumeric characters, hyphens, underscores or // forward slashes. References don't have any restrictions. -const nsPathRegExp = /^([\w-_\/]+)::(.+)$/; +const nsPathRegExp = /^(?:(?[\w_\/-]+)::)(?.+)$/; export const hydratedIslands = new WeakSet(); @@ -95,25 +95,27 @@ export function toVdom( root: Node ): Array< ComponentChild > { if ( attributeName === ignoreAttr ) { ignore = true; } else { - const [ ns, value ] = nsPathRegExp - .exec( attributes[ i ].value ) - ?.slice( 1 ) ?? [ null, attributes[ i ].value ]; - let parsedValue: Record< string, unknown > | null = null; + const regexResult = nsPathRegExp.exec( + attributes[ i ].value + )?.groups; + const namespace = regexResult?.nameSpace ?? null; + let value: any = + regexResult?.value ?? attributes[ i ].value; try { - parsedValue = value ? JSON.parse( value ) : null; + value = value && JSON.parse( value ); } catch ( e ) {} if ( attributeName === islandAttr ) { island = true; - const namespace = + const islandNamespace = // eslint-disable-next-line no-nested-ternary - typeof parsedValue?.namespace === 'string' - ? parsedValue.namespace - : typeof value === 'string' + typeof value === 'string' ? value + : typeof value?.namespace === 'string' + ? value.namespace : null; - namespaces.push( namespace ); + namespaces.push( islandNamespace ); } else { - directives.push( [ attributeName, ns, value ] ); + directives.push( [ attributeName, namespace, value ] ); } } } else if ( attributeName === 'ref' ) { From 6dd3b341ad6210a6ca53d8a5b267690b72be89db Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 15 May 2024 19:33:58 +0200 Subject: [PATCH 20/23] Remove redundant group from ns regex --- packages/interactivity/src/vdom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 4933682749fb90..1466d478b97c41 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -32,7 +32,7 @@ const directiveParser = new RegExp( // the reference, separated by `::`, like `some-namespace::state.somePath`. // Namespaces can contain any alphanumeric characters, hyphens, underscores or // forward slashes. References don't have any restrictions. -const nsPathRegExp = /^(?:(?[\w_\/-]+)::)(?.+)$/; +const nsPathRegExp = /^(?[\w_\/-]+)::(?.+)$/; export const hydratedIslands = new WeakSet(); From b728830f6e91ee0308b4eab6ed0576f26c6d91d1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 15 May 2024 19:36:51 +0200 Subject: [PATCH 21/23] Update packages/interactivity/CHANGELOG.md --- packages/interactivity/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 4c54801c0210d5..01097504d7064a 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -4,7 +4,7 @@ ### Enhancements -- Strict type checking. ([#59865] (https://github.com/WordPress/gutenberg/pull/59865/)) +- Strict type checking: fix some missing nulls in published types ([#59865](https://github.com/WordPress/gutenberg/pull/59865/)). ### Bug Fixes From 1f53865105430ad5811456d957184f084a92a4f2 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 15 May 2024 19:43:21 +0200 Subject: [PATCH 22/23] Still learning to type --- packages/interactivity/src/vdom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 1466d478b97c41..1af48c3c0c7ca9 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -98,7 +98,7 @@ export function toVdom( root: Node ): Array< ComponentChild > { const regexResult = nsPathRegExp.exec( attributes[ i ].value )?.groups; - const namespace = regexResult?.nameSpace ?? null; + const namespace = regexResult?.namespace ?? null; let value: any = regexResult?.value ?? attributes[ i ].value; try { From 1db22687abbc9b21e1e81289925841081278b6f7 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 15 May 2024 19:55:02 +0200 Subject: [PATCH 23/23] Better variable naming --- packages/interactivity/src/vdom.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 1af48c3c0c7ca9..d5238cde49d8e7 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -95,12 +95,12 @@ export function toVdom( root: Node ): Array< ComponentChild > { if ( attributeName === ignoreAttr ) { ignore = true; } else { - const regexResult = nsPathRegExp.exec( + const regexCaptureGroups = nsPathRegExp.exec( attributes[ i ].value )?.groups; - const namespace = regexResult?.namespace ?? null; + const namespace = regexCaptureGroups?.namespace ?? null; let value: any = - regexResult?.value ?? attributes[ i ].value; + regexCaptureGroups?.value ?? attributes[ i ].value; try { value = value && JSON.parse( value ); } catch ( e ) {}