From 726d58723338abc26d1eaffb3dedca44b8408084 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:17:42 +0200 Subject: [PATCH 1/6] strict nulls --- packages/interactivity/src/vdom.ts | 78 +++++++++++++++++++--------- packages/interactivity/tsconfig.json | 1 - 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 5a997993668094..2ab176b7acf0dc 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -1,16 +1,18 @@ /** * External dependencies */ -import { h } from 'preact'; +import { h, type VNode, type JSX } from 'preact'; /** * Internal dependencies */ import { directivePrefix as p } from './constants'; +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. @@ -38,33 +40,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 as Text ).data, null ]; + } - if ( nodeType === 3 ) return [ node.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.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; @@ -81,7 +99,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; @@ -100,22 +118,29 @@ export function toVdom( root ) { props[ n ] = attributes[ i ].value; } - if ( ignore && ! island ) + 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 ); + } + + if ( island ) { + hydratedIslands.add( elementNode ); + } if ( directives.length ) { props.__directives = directives.reduce( ( obj, [ name, ns, value ] ) => { const [ , prefix, suffix = 'default' ] = - directiveParser.exec( name ); + directiveParser.exec( name )!; + if ( ! obj[ prefix ] ) obj[ prefix ] = []; + obj[ prefix ].push( { namespace: ns ?? currentNamespace(), value, @@ -127,16 +152,19 @@ 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 ) { while ( child ) { const [ vnode, nextChild ] = walk( child ); - if ( vnode ) children.push( vnode ); + if ( vnode ) { + children.push( vnode ); + } child = nextChild || treeWalker.nextSibling(); } treeWalker.parentNode(); @@ -144,9 +172,11 @@ export function toVdom( root ) { } // Restore previous namespace. - if ( island ) namespaces.pop(); + if ( island ) { + 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 12ccb291ddb7460015f1237c825bd4b82d2d2a98 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:20:38 +0200 Subject: [PATCH 2/6] 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 8e8e783ae125cb..42e07f60e0bf58 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -471,6 +471,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 2c65ee3d923629c10c861e6394496b58530bbc6e Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:25:57 +0200 Subject: [PATCH 3/6] 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 8b6da05b938447..6919575e97bd3b 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 () => { @@ -271,7 +271,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 9f302362fdbdf702b46622698a1426569bfa4f70 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:27:11 +0200 Subject: [PATCH 4/6] 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 2aad8c23d1db12..ff70b4ab297085 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 ) ) { @@ -332,12 +332,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 1f35a8d29b966b1762e6ec149c540c85ccc3cf37 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:29:49 +0200 Subject: [PATCH 5/6] 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 ff8b8180557f93..39be45ecd94ada 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -72,7 +72,7 @@ interface DirectiveOptions { interface Scope { evaluate: Evaluate; - context?: object; + context: object; ref: RefObject< HTMLElement >; attributes: createElement.JSX.HTMLAttributes; } From b71e89784d78f2c3e8e0bc937d6aa5bad7d432c9 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 10 Apr 2024 19:31:49 +0200 Subject: [PATCH 6/6] 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 2ab176b7acf0dc..692fd0dffea27d 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -106,7 +106,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 ] );