From a6064f2e57f2be2ed1750eb97138d1947be2700a Mon Sep 17 00:00:00 2001 From: Chris Sauve Date: Wed, 3 Jan 2024 22:47:06 -0500 Subject: [PATCH] Backport all changes --- examples/kitchen-sink-vite/app/host.ts | 2 +- examples/kitchen-sink-vite/app/types.ts | 4 +- examples/minimal-iframes/app/index.html | 2 +- examples/vanilla-dom/app/host.js | 2 +- package.json | 1 + .../source/{callback.ts => connection.ts} | 23 +- packages/core/source/constants.ts | 2 +- packages/core/source/elements.ts | 3 +- .../core/source/elements/RemoteElement.ts | 87 +++-- .../source/elements/RemoteMutationObserver.ts | 33 +- .../source/elements/RemoteReceiverElement.ts | 8 +- .../core/source/elements/RemoteRootElement.ts | 36 +- packages/core/source/elements/internals.ts | 63 +-- packages/core/source/index.ts | 8 +- packages/core/source/polyfill.ts | 31 +- packages/core/source/receiver/basic.ts | 42 +- packages/core/source/receiver/dom.ts | 26 +- packages/core/source/tests/elements.test.ts | 87 ++--- packages/core/source/types.ts | 7 +- packages/polyfill/package.json | 3 +- packages/preact/package.json | 20 +- packages/preact/source/host.ts | 4 - .../preact/source/host/RemoteTextRenderer.tsx | 14 - packages/preact/source/host/component.tsx | 71 +++- packages/preact/source/host/node.tsx | 5 +- packages/preact/source/tests/e2e.test.tsx | 219 +++++++++++ packages/preact/vite.config.js | 22 ++ packages/react/package.json | 27 +- packages/react/source/host/component.tsx | 75 +++- packages/react/source/tests/e2e.test.tsx | 220 +++++++++++ packages/react/vite.config.js | 21 + packages/signals/package.json | 10 +- packages/signals/source/receiver.ts | 39 +- pnpm-lock.yaml | 361 ++++++++++++++++-- 34 files changed, 1276 insertions(+), 302 deletions(-) rename packages/core/source/{callback.ts => connection.ts} (77%) delete mode 100644 packages/preact/source/host/RemoteTextRenderer.tsx create mode 100644 packages/preact/source/tests/e2e.test.tsx create mode 100644 packages/preact/vite.config.js create mode 100644 packages/react/source/tests/e2e.test.tsx create mode 100644 packages/react/vite.config.js diff --git a/examples/kitchen-sink-vite/app/host.ts b/examples/kitchen-sink-vite/app/host.ts index 06f85fba..a5e6301b 100644 --- a/examples/kitchen-sink-vite/app/host.ts +++ b/examples/kitchen-sink-vite/app/host.ts @@ -52,7 +52,7 @@ receiver.connect(uiRoot); // provides a `render()` function that will be called in response to this // method, with the `Endpoint` taking care of serializing arguments over // `postMessage()` to the remote context. -await workerSandbox.render(receiver.receive, { +await workerSandbox.render(receiver.connection, { sandbox: 'worker', framework: 'htm', async alert(content) { diff --git a/examples/kitchen-sink-vite/app/types.ts b/examples/kitchen-sink-vite/app/types.ts index ac489728..bf8a475a 100644 --- a/examples/kitchen-sink-vite/app/types.ts +++ b/examples/kitchen-sink-vite/app/types.ts @@ -1,4 +1,4 @@ -import type {RemoteMutationCallback} from '@remote-dom/core'; +import type {RemoteConnection} from '@remote-dom/core'; export type RenderFramework = 'vanilla' | 'htm'; export type RenderSandbox = 'iframe' | 'worker'; @@ -10,5 +10,5 @@ export interface RenderApi { } export interface SandboxApi { - render(callback: RemoteMutationCallback, api: RenderApi): Promise; + render(connection: RemoteConnection, api: RenderApi): Promise; } diff --git a/examples/minimal-iframes/app/index.html b/examples/minimal-iframes/app/index.html index 193b6146..144da884 100644 --- a/examples/minimal-iframes/app/index.html +++ b/examples/minimal-iframes/app/index.html @@ -69,7 +69,7 @@ window.addEventListener('message', ({source, data}) => { if (source !== iframe.contentWindow) return; - receiver.receive(data); + receiver.connection.mutate(data); }); diff --git a/examples/vanilla-dom/app/host.js b/examples/vanilla-dom/app/host.js index 626880b0..e0bfe78d 100644 --- a/examples/vanilla-dom/app/host.js +++ b/examples/vanilla-dom/app/host.js @@ -43,6 +43,6 @@ receiver.bind(uiRoot); // provides a `render()` function that will be called in response to this // method, with the `Endpoint` taking care of serializing arguments over // `postMessage()` to the remote context. -await remoteEndpoint.call.render(receiver.receive, { +await remoteEndpoint.call.render(receiver.connection, { getMessage: () => textField.value, }); diff --git a/package.json b/package.json index a06888d2..da924d0c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@quilted/typescript": "^0.3.0", "@quilted/vite": "^0.1.17", "@types/node": "~20.9.0", + "jsdom": "^23.0.1", "prettier": "^3.1.0", "rollup": "^4.9.0", "tsx": "^4.7.0", diff --git a/packages/core/source/callback.ts b/packages/core/source/connection.ts similarity index 77% rename from packages/core/source/callback.ts rename to packages/core/source/connection.ts index 66420d4c..113221b2 100644 --- a/packages/core/source/callback.ts +++ b/packages/core/source/connection.ts @@ -5,7 +5,7 @@ import { MUTATION_TYPE_UPDATE_PROPERTY, } from './constants.ts'; import type { - RemoteMutationCallback, + RemoteConnection, RemoteMutationRecord, RemoteMutationRecordInsertChild, RemoteMutationRecordRemoveChild, @@ -13,9 +13,10 @@ import type { RemoteMutationRecordUpdateProperty, } from './types.ts'; -export type {RemoteMutationCallback}; +export type {RemoteConnection}; -export interface RemoteMutationHandler { +export interface RemoteConnectionHandler { + call: RemoteConnection['call']; insertChild( id: RemoteMutationRecordInsertChild[1], child: RemoteMutationRecordInsertChild[2], @@ -36,12 +37,13 @@ export interface RemoteMutationHandler { ): void; } -export function createRemoteMutationCallback({ +export function createRemoteConnection({ + call, insertChild, removeChild, updateText, updateProperty, -}: RemoteMutationHandler): RemoteMutationCallback { +}: RemoteConnectionHandler): RemoteConnection { const messageMap = new Map any>([ [MUTATION_TYPE_INSERT_CHILD, insertChild], [MUTATION_TYPE_REMOVE_CHILD, removeChild], @@ -49,9 +51,12 @@ export function createRemoteMutationCallback({ [MUTATION_TYPE_UPDATE_PROPERTY, updateProperty], ]); - return function handler(records) { - for (const [type, ...args] of records) { - messageMap.get(type)!(...args); - } + return { + call, + mutate(records) { + for (const [type, ...args] of records) { + messageMap.get(type)!(...args); + } + }, }; } diff --git a/packages/core/source/constants.ts b/packages/core/source/constants.ts index 5188a644..5362ca58 100644 --- a/packages/core/source/constants.ts +++ b/packages/core/source/constants.ts @@ -9,6 +9,6 @@ export const MUTATION_TYPE_UPDATE_TEXT = 2; export const MUTATION_TYPE_UPDATE_PROPERTY = 3; export const REMOTE_ID = Symbol.for('remote.id'); -export const REMOTE_CALLBACK = Symbol.for('remote.callback'); +export const REMOTE_CONNECTION = Symbol.for('remote.connection'); export const REMOTE_PROPERTIES = Symbol.for('remote.properties'); export const ROOT_ID = '_root'; diff --git a/packages/core/source/elements.ts b/packages/core/source/elements.ts index 5c22c029..10302b87 100644 --- a/packages/core/source/elements.ts +++ b/packages/core/source/elements.ts @@ -23,6 +23,7 @@ export { disconnectRemoteNode, serializeRemoteNode, updateRemoteElementProperty, + callRemoteElementMethod, } from './elements/internals.ts'; export {remoteSlots} from './elements/decorators/remote-slots.ts'; @@ -32,4 +33,4 @@ export {customElement} from './elements/decorators/custom-element.ts'; export {BooleanOrString} from './elements/property-types/BooleanOrString.ts'; -export type {RemoteMutationCallback} from './callback.ts'; +export type {RemoteConnection} from './types.ts'; diff --git a/packages/core/source/elements/RemoteElement.ts b/packages/core/source/elements/RemoteElement.ts index 264b7392..1f94b8ff 100644 --- a/packages/core/source/elements/RemoteElement.ts +++ b/packages/core/source/elements/RemoteElement.ts @@ -1,6 +1,9 @@ import {REMOTE_PROPERTIES} from '../constants.ts'; import {RemoteEvent} from './RemoteEvent.ts'; -import {updateRemoteElementProperty} from './internals.ts'; +import { + updateRemoteElementProperty, + callRemoteElementMethod, +} from './internals.ts'; export interface RemoteElementPropertyType { parse?(value: string | unknown): Value; @@ -24,7 +27,7 @@ export interface RemoteElementPropertyDefinition { default?: Value; } -interface NormalizedRemoteElementPropertyDefinition { +interface RemoteElementPropertyNormalizedDefinition { name: string; type: RemoteElementPropertyTypeOrBuiltIn; alias?: string[]; @@ -43,7 +46,7 @@ export type RemoteElementPropertiesDefinition< export interface RemoteElementSlotDefinition {} -interface NormalizedRemoteElementSlotDefinition {} +interface RemoteElementSlotNormalizedDefinition {} export type RemoteElementSlotsDefinition< Slots extends Record = {}, @@ -52,35 +55,42 @@ export type RemoteElementSlotsDefinition< }; export type RemotePropertiesFromElementConstructor = T extends { - new (): RemoteElement; + new (): RemoteElement; } ? Properties : never; export type RemoteSlotsFromElementConstructor = T extends { - new (): RemoteElement; + new (): RemoteElement; } ? Slots : never; +export type RemoteMethodsFromElementConstructor = T extends { + new (): RemoteElement; +} + ? Methods + : never; + export type RemoteElementConstructor< Properties extends Record = {}, Slots extends Record = {}, + Methods extends Record any> = {}, > = { - new (): RemoteElement & Properties; + new (): RemoteElement & Properties & Methods; readonly remoteSlots?: | RemoteElementSlotsDefinition | readonly (keyof Slots)[]; readonly remoteSlotDefinitions: Map< string, - NormalizedRemoteElementSlotDefinition + RemoteElementSlotNormalizedDefinition >; readonly remoteProperties?: | RemoteElementPropertiesDefinition | readonly (keyof Properties)[]; readonly remotePropertyDefinitions: Map< string, - NormalizedRemoteElementPropertyDefinition + RemoteElementPropertyNormalizedDefinition >; createProperty( name: string, @@ -91,25 +101,51 @@ export type RemoteElementConstructor< export interface RemoteElementCreatorOptions< Properties extends Record = {}, Slots extends Record = {}, + Methods extends Record any> = {}, > { - slots?: RemoteElementConstructor['remoteSlots']; - properties?: RemoteElementConstructor['remoteProperties']; + slots?: RemoteElementConstructor['remoteSlots']; + properties?: RemoteElementConstructor< + Properties, + Slots, + Methods + >['remoteProperties']; + methods?: Methods | keyof Methods[]; } export function createRemoteElement< Properties extends Record = {}, Slots extends Record = {}, + Methods extends Record any> = {}, >({ slots, properties, + methods, }: RemoteElementCreatorOptions< Properties, - Slots -> = {}): RemoteElementConstructor { - return class extends RemoteElement { + Slots, + Methods +> = {}): RemoteElementConstructor { + const RemoteElementConstructor = class extends RemoteElement< + Properties, + Slots + > { static readonly remoteSlots = slots; static readonly remoteProperties = properties; } as any; + + if (methods != null) { + if (Array.isArray(methods)) { + for (const method of methods) { + RemoteElementConstructor.prototype[method] = function (...args: any[]) { + return callRemoteElementMethod(this, method, ...args); + }; + } + } else { + Object.assign(RemoteElementConstructor.prototype, methods); + } + } + + return RemoteElementConstructor; } const SLOT_PROPERTY = 'slot'; @@ -131,11 +167,13 @@ type RemoteEventListenerRecord = [ export abstract class RemoteElement< Properties extends Record = {}, Slots extends Record = {}, + Methods extends Record any> = {}, > extends HTMLElement { static readonly slottable = true; static readonly remoteSlots?: any; static readonly remoteProperties?: any; + static readonly remoteMethods?: any; static get observedAttributes() { return this.finalize().__observedAttributes; @@ -143,14 +181,14 @@ export abstract class RemoteElement< static get remotePropertyDefinitions(): Map< string, - NormalizedRemoteElementPropertyDefinition + RemoteElementPropertyNormalizedDefinition > { return this.finalize().__remotePropertyDefinitions; } static get remoteSlotDefinitions(): Map< string, - NormalizedRemoteElementSlotDefinition + RemoteElementSlotNormalizedDefinition > { return this.finalize().__remoteSlotDefinitions; } @@ -161,11 +199,11 @@ export abstract class RemoteElement< private static readonly __eventToPropertyMap = new Map(); private static readonly __remotePropertyDefinitions = new Map< string, - NormalizedRemoteElementPropertyDefinition + RemoteElementPropertyNormalizedDefinition >(); private static readonly __remoteSlotDefinitions = new Map< string, - NormalizedRemoteElementSlotDefinition + RemoteElementSlotNormalizedDefinition >(); static createProperty( @@ -183,6 +221,7 @@ export abstract class RemoteElement< } protected static finalize(): typeof this { + // eslint-disable-next-line no-prototype-builtins if (this.hasOwnProperty('__finalized')) { return this; } @@ -200,11 +239,11 @@ export abstract class RemoteElement< const eventToPropertyMap = new Map(); const remoteSlotDefinitions = new Map< string, - NormalizedRemoteElementSlotDefinition + RemoteElementSlotNormalizedDefinition >(); const remotePropertyDefinitions = new Map< string, - NormalizedRemoteElementPropertyDefinition + RemoteElementPropertyNormalizedDefinition >(); if (typeof SuperConstructor.finalize === 'function') { @@ -308,6 +347,9 @@ export abstract class RemoteElement< /** @internal */ __properties?: Properties; + /** @internal */ + __methods?: Methods; + private [REMOTE_PROPERTIES]!: Properties; private [REMOTE_EVENTS]?: { readonly events: Map; @@ -338,6 +380,7 @@ export abstract class RemoteElement< // Don’t override actual accessors. This is handled by the // `remoteProperty()` decorator applied to the accessor. + // eslint-disable-next-line no-prototype-builtins if (Object.getPrototypeOf(this).hasOwnProperty(property)) { continue; } @@ -527,7 +570,7 @@ function saveRemoteProperty( observedAttributes: string[], remotePropertyDefinitions: Map< string, - NormalizedRemoteElementPropertyDefinition + RemoteElementPropertyNormalizedDefinition >, attributeToPropertyMap: Map, eventToPropertyMap: Map, @@ -592,7 +635,7 @@ function saveRemoteProperty( eventToPropertyMap.set(eventName, name); } - const definition: NormalizedRemoteElementPropertyDefinition = { + const definition: RemoteElementPropertyNormalizedDefinition = { name, type, alias, @@ -620,7 +663,7 @@ function convertAttributeValueToProperty( switch (type) { case Boolean: - return value !== 'false'; + return value != null && value !== 'false'; case Object: case Array: try { diff --git a/packages/core/source/elements/RemoteMutationObserver.ts b/packages/core/source/elements/RemoteMutationObserver.ts index 724ce303..a5714fa8 100644 --- a/packages/core/source/elements/RemoteMutationObserver.ts +++ b/packages/core/source/elements/RemoteMutationObserver.ts @@ -13,10 +13,10 @@ import { MUTATION_TYPE_UPDATE_TEXT, MUTATION_TYPE_UPDATE_PROPERTY, } from '../constants.ts'; -import type {RemoteMutationCallback, RemoteMutationRecord} from '../types.ts'; +import type {RemoteConnection, RemoteMutationRecord} from '../types.ts'; export class RemoteMutationObserver extends MutationObserver { - constructor(private readonly callback: RemoteMutationCallback) { + constructor(private readonly connection: RemoteConnection) { super((records) => { const remoteRecords: RemoteMutationRecord[] = []; @@ -39,7 +39,7 @@ export class RemoteMutationObserver extends MutationObserver { }); record.addedNodes.forEach((node, index) => { - connectRemoteNode(node, callback); + connectRemoteNode(node, connection); remoteRecords.push([ MUTATION_TYPE_INSERT_CHILD, @@ -68,12 +68,12 @@ export class RemoteMutationObserver extends MutationObserver { } } - callback(remoteRecords); + connection.mutate(remoteRecords); }); } observe( - target: Node & {[REMOTE_ID]?: string}, + target: Node, options?: MutationObserverInit & { /** * Whether to send the initial state of the tree to the mutation @@ -84,19 +84,24 @@ export class RemoteMutationObserver extends MutationObserver { initial?: boolean; }, ) { - if (target[REMOTE_ID] == null) { - target[REMOTE_ID] = ROOT_ID; - } + Object.defineProperty(target, REMOTE_ID, {value: ROOT_ID}); + + if (options?.initial !== false && target.childNodes.length > 0) { + const records: RemoteMutationRecord[] = []; + + for (let i = 0; i < target.childNodes.length; i++) { + const node = target.childNodes[i]!; + connectRemoteNode(node, this.connection); - if (options?.initial !== false) { - this.callback( - Array.from(target.childNodes, (node, index) => [ + records.push([ MUTATION_TYPE_INSERT_CHILD, ROOT_ID, serializeRemoteNode(node), - index, - ]), - ); + i, + ]); + } + + this.connection.mutate(records); } super.observe(target, { diff --git a/packages/core/source/elements/RemoteReceiverElement.ts b/packages/core/source/elements/RemoteReceiverElement.ts index 6e1f51be..94f65b7f 100644 --- a/packages/core/source/elements/RemoteReceiverElement.ts +++ b/packages/core/source/elements/RemoteReceiverElement.ts @@ -1,11 +1,7 @@ import {DOMRemoteReceiver} from '../receiver/dom.ts'; export class RemoteReceiverElement extends HTMLElement { - readonly receive: DOMRemoteReceiver['receive']; - - get callback() { - return this.receive; - } + readonly connection: DOMRemoteReceiver['connection']; retain?: (value: any) => void; release?: (value: any) => void; @@ -19,6 +15,6 @@ export class RemoteReceiverElement extends HTMLElement { }); receiver.connect(this); - this.receive = receiver.receive; + this.connection = receiver.connection; } } diff --git a/packages/core/source/elements/RemoteRootElement.ts b/packages/core/source/elements/RemoteRootElement.ts index 560520eb..dad295ca 100644 --- a/packages/core/source/elements/RemoteRootElement.ts +++ b/packages/core/source/elements/RemoteRootElement.ts @@ -1,38 +1,36 @@ import { - REMOTE_ID, - REMOTE_CALLBACK, ROOT_ID, + REMOTE_ID, + REMOTE_CONNECTION, MUTATION_TYPE_INSERT_CHILD, } from '../constants.ts'; -import type {RemoteMutationCallback, RemoteMutationRecord} from '../types.ts'; +import type {RemoteConnection, RemoteMutationRecord} from '../types.ts'; import {connectRemoteNode, serializeRemoteNode} from './internals.ts'; export class RemoteRootElement extends HTMLElement { readonly [REMOTE_ID] = ROOT_ID; - [REMOTE_CALLBACK]?: RemoteMutationCallback; + [REMOTE_CONNECTION]?: RemoteConnection; - connect(callback: RemoteMutationCallback): void { - if (this[REMOTE_CALLBACK] === callback) return; + connect(connection: RemoteConnection): void { + if (this[REMOTE_CONNECTION] === connection) return; - connectRemoteNode(this, callback); + connectRemoteNode(this, connection); - if (this.childNodes.length > 0) { - const records: RemoteMutationRecord[] = []; + if (this.childNodes.length === 0) return; - for (let i = 0; i < this.childNodes.length; i++) { - const node = this.childNodes[i]!; + const records: RemoteMutationRecord[] = []; - records.push([ - MUTATION_TYPE_INSERT_CHILD, - this[REMOTE_ID], - serializeRemoteNode(node), - i, - ]); - } + for (let i = 0; i < this.childNodes.length; i++) { + const node = this.childNodes[i]!; - callback(records); + records.push([ + MUTATION_TYPE_INSERT_CHILD, + ROOT_ID, + serializeRemoteNode(node), + i, + ]); } } } diff --git a/packages/core/source/elements/internals.ts b/packages/core/source/elements/internals.ts index d65883a9..af5758f3 100644 --- a/packages/core/source/elements/internals.ts +++ b/packages/core/source/elements/internals.ts @@ -1,17 +1,20 @@ import { REMOTE_ID, - REMOTE_CALLBACK, + REMOTE_CONNECTION, REMOTE_PROPERTIES, MUTATION_TYPE_UPDATE_PROPERTY, } from '../constants.ts'; -import type { - RemoteMutationCallback, - RemoteNodeSerialization, -} from '../types.ts'; +import type {RemoteConnection, RemoteNodeSerialization} from '../types.ts'; let id = 0; -export function remoteId(node: Node & {[REMOTE_ID]?: string}) { +export type RemoteConnectedNode = T & { + [REMOTE_ID]?: string; + [REMOTE_CONNECTION]?: RemoteConnection; + [REMOTE_PROPERTIES]?: Record; +}; + +export function remoteId(node: RemoteConnectedNode) { if (node[REMOTE_ID] == null) { node[REMOTE_ID] = String(id++); } @@ -19,9 +22,7 @@ export function remoteId(node: Node & {[REMOTE_ID]?: string}) { return node[REMOTE_ID]; } -export function remoteProperties( - node: Node & {[REMOTE_PROPERTIES]?: Record}, -) { +export function remoteProperties(node: RemoteConnectedNode) { if (node[REMOTE_PROPERTIES] != null) return node[REMOTE_PROPERTIES]; if ((node as any).attributes == null) return undefined; @@ -39,7 +40,7 @@ export function updateRemoteElementProperty( property: string, value: unknown, ) { - let properties = (node as any)[REMOTE_PROPERTIES]; + let properties = (node as RemoteConnectedNode)[REMOTE_PROPERTIES]; if (properties == null) { properties = {}; @@ -50,32 +51,34 @@ export function updateRemoteElementProperty( properties[property] = value; - const callback = (node as any)[REMOTE_CALLBACK]; + const connection = (node as RemoteConnectedNode)[REMOTE_CONNECTION]; - if (callback == null) return; + if (connection == null) return; - callback([[MUTATION_TYPE_UPDATE_PROPERTY, remoteId(node), property, value]]); + connection.mutate([ + [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(node), property, value], + ]); } export function connectRemoteNode( - node: Node, - callback: RemoteMutationCallback, + node: RemoteConnectedNode, + connection: RemoteConnection, ) { - if ((node as any)[REMOTE_CALLBACK] === callback) return; + if ((node as any)[REMOTE_CONNECTION] === connection) return; - (node as any)[REMOTE_CALLBACK] = callback; + (node as any)[REMOTE_CONNECTION] = connection; if (node.childNodes) { for (let i = 0; i < node.childNodes.length; i++) { - connectRemoteNode(node.childNodes[i]!, callback); + connectRemoteNode(node.childNodes[i]!, connection); } } } -export function disconnectRemoteNode(node: Node) { - if ((node as any)[REMOTE_CALLBACK] == null) return; +export function disconnectRemoteNode(node: RemoteConnectedNode) { + if ((node as any)[REMOTE_CONNECTION] == null) return; - (node as any)[REMOTE_CALLBACK] = undefined; + (node as any)[REMOTE_CONNECTION] = undefined; if (node.childNodes) { for (let i = 0; i < node.childNodes.length; i++) { @@ -94,13 +97,14 @@ export function serializeRemoteNode(node: Node): RemoteNodeSerialization { id: remoteId(node), type: nodeType, element: (node as Element).localName, - properties: remoteProperties(node), + properties: Object.assign({}, remoteProperties(node)), children: Array.from(node.childNodes).map(serializeRemoteNode), }; } // TextNode case 3: // Comment + // eslint-disable-next-line no-fallthrough case 8: { return { id: remoteId(node), @@ -117,3 +121,18 @@ export function serializeRemoteNode(node: Node): RemoteNodeSerialization { } } } + +export function callRemoteElementMethod( + node: Element, + method: string, + ...args: unknown[] +) { + const id = (node as RemoteConnectedNode)[REMOTE_ID]; + const connection = (node as RemoteConnectedNode)[REMOTE_CONNECTION]; + + if (id == null || connection == null) { + throw new Error(`Cannot call method ${method} on an unconnected node`); + } + + return connection.call(id, method, ...args); +} diff --git a/packages/core/source/index.ts b/packages/core/source/index.ts index 8c311c86..63751dcd 100644 --- a/packages/core/source/index.ts +++ b/packages/core/source/index.ts @@ -1,13 +1,13 @@ export { - createRemoteMutationCallback, - type RemoteMutationHandler, -} from './callback.ts'; + createRemoteConnection, + type RemoteConnectionHandler, +} from './connection.ts'; export * from './types.ts'; export { ROOT_ID, REMOTE_ID, - REMOTE_CALLBACK, + REMOTE_CONNECTION, REMOTE_PROPERTIES, NODE_TYPE_COMMENT, NODE_TYPE_ELEMENT, diff --git a/packages/core/source/polyfill.ts b/packages/core/source/polyfill.ts index ce998723..61d2fa4f 100644 --- a/packages/core/source/polyfill.ts +++ b/packages/core/source/polyfill.ts @@ -6,18 +6,19 @@ import { } from '@remote-dom/polyfill'; import { - REMOTE_CALLBACK, + REMOTE_CONNECTION, REMOTE_PROPERTIES, MUTATION_TYPE_INSERT_CHILD, MUTATION_TYPE_REMOVE_CHILD, - MUTATION_TYPE_UPDATE_TEXT, MUTATION_TYPE_UPDATE_PROPERTY, + MUTATION_TYPE_UPDATE_TEXT, } from './constants.ts'; import { remoteId, connectRemoteNode, disconnectRemoteNode, serializeRemoteNode, + type RemoteConnectedNode, } from './elements/internals.ts'; const window = new Window(); @@ -25,12 +26,12 @@ const window = new Window(); installWindowAsGlobal(window); hooks.insertChild = (parent, node, index) => { - const callback = (parent as any)[REMOTE_CALLBACK]; - if (callback == null) return; + const connection = (parent as RemoteConnectedNode)[REMOTE_CONNECTION]; + if (connection == null) return; - connectRemoteNode(node, callback); + connectRemoteNode(node, connection); - callback([ + connection.mutate([ [ MUTATION_TYPE_INSERT_CHILD, remoteId(parent), @@ -41,28 +42,28 @@ hooks.insertChild = (parent, node, index) => { }; hooks.removeChild = (parent, node, index) => { - const callback = (parent as any)[REMOTE_CALLBACK]; - if (callback == null) return; + const connection = (parent as RemoteConnectedNode)[REMOTE_CONNECTION]; + if (connection == null) return; disconnectRemoteNode(node); - callback([[MUTATION_TYPE_REMOVE_CHILD, remoteId(parent), index]]); + connection.mutate([[MUTATION_TYPE_REMOVE_CHILD, remoteId(parent), index]]); }; hooks.setText = (text, data) => { - const callback = (text as any)[REMOTE_CALLBACK]; - if (callback == null) return; + const connection = (text as RemoteConnectedNode)[REMOTE_CONNECTION]; + if (connection == null) return; - callback([[MUTATION_TYPE_UPDATE_TEXT, remoteId(text), data]]); + connection.mutate([[MUTATION_TYPE_UPDATE_TEXT, remoteId(text), data]]); }; hooks.setAttribute = (element, name, value) => { - const callback = (element as any)[REMOTE_CALLBACK]; - const properties = (element as any)[REMOTE_PROPERTIES]; + const callback = (element as RemoteConnectedNode)[REMOTE_CONNECTION]; + const properties = (element as RemoteConnectedNode)[REMOTE_PROPERTIES]; if (callback == null || properties != null) return; - callback([ + callback.mutate([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), diff --git a/packages/core/source/receiver/basic.ts b/packages/core/source/receiver/basic.ts index 2d073ff2..9a9ce01a 100644 --- a/packages/core/source/receiver/basic.ts +++ b/packages/core/source/receiver/basic.ts @@ -1,7 +1,4 @@ -import { - createRemoteMutationCallback, - type RemoteMutationCallback, -} from '../callback.ts'; +import {createRemoteConnection, type RemoteConnection} from '../connection.ts'; import { NODE_TYPE_COMMENT, NODE_TYPE_ELEMENT, @@ -69,17 +66,29 @@ export class RemoteReceiver { >(); private readonly parents = new Map(); + private readonly implementations = new Map< + string, + Record unknown> + >(); - readonly receive: RemoteMutationCallback; - - get callback() { - return this.receive; - } + readonly connection: RemoteConnection; constructor({retain, release}: RemoteReceiverOptions = {}) { const {attached, parents, subscribers} = this; - this.receive = createRemoteMutationCallback({ + this.connection = createRemoteConnection({ + call: (id, method, ...args) => { + const implementation = this.implementations.get(id); + const implementationMethod = implementation?.[method]; + + if (typeof implementationMethod !== 'function') { + throw new Error( + `Node ${id} does not implement the ${method}() method`, + ); + } + + return implementationMethod(...args); + }, insertChild: (id, child, index) => { const parent = attached.get(id) as Writable; @@ -241,8 +250,19 @@ export class RemoteReceiver { return this.attached.get(id) as any; } + implement( + {id}: Pick, + implementation?: Record unknown> | null, + ) { + if (implementation == null) { + this.implementations.delete(id); + } else { + this.implementations.set(id, implementation); + } + } + subscribe( - {id}: T, + {id}: Pick, subscriber: (value: T) => void, {signal}: {signal?: AbortSignal} = {}, ) { diff --git a/packages/core/source/receiver/dom.ts b/packages/core/source/receiver/dom.ts index 96ed38ee..12e6555a 100644 --- a/packages/core/source/receiver/dom.ts +++ b/packages/core/source/receiver/dom.ts @@ -1,7 +1,4 @@ -import { - createRemoteMutationCallback, - type RemoteMutationCallback, -} from '../callback.ts'; +import {createRemoteConnection, type RemoteConnection} from '../connection.ts'; import { NODE_TYPE_TEXT, NODE_TYPE_COMMENT, @@ -14,24 +11,19 @@ import type {RemoteNodeSerialization} from '../types.ts'; import type {RemoteReceiverOptions} from './shared.ts'; export class DOMRemoteReceiver { - readonly root: DocumentFragment | Element; - readonly receive: RemoteMutationCallback; - - get callback() { - return this.receive; - } + readonly root: DocumentFragment | Element = document.createDocumentFragment(); + readonly connection: RemoteConnection; private readonly attached = new Map(); - constructor({ - root = document.createDocumentFragment(), - retain, - release, - }: RemoteReceiverOptions & {root?: DocumentFragment | Element} = {}) { + constructor({retain, release}: RemoteReceiverOptions = {}) { const {attached} = this; - this.root = root; - this.receive = createRemoteMutationCallback({ + this.connection = createRemoteConnection({ + call(id, method, ...args) { + const element = attached.get(id)!; + return (element as any)[method](...args); + }, insertChild: (id, child, index) => { const parent = id === ROOT_ID ? this.root : attached.get(id)!; parent.insertBefore(attach(child), parent.childNodes[index] || null); diff --git a/packages/core/source/tests/elements.test.ts b/packages/core/source/tests/elements.test.ts index ae0b9af7..8bbe5dd4 100644 --- a/packages/core/source/tests/elements.test.ts +++ b/packages/core/source/tests/elements.test.ts @@ -1,5 +1,5 @@ import '../polyfill.ts'; -import {describe, it, expect, vi, type Mock} from 'vitest'; +import {describe, it, expect, vi, type MockedObject} from 'vitest'; import { RemoteElement, @@ -12,7 +12,7 @@ import { import {RemoteReceiver} from '../receiver/basic.ts'; import {REMOTE_ID, MUTATION_TYPE_UPDATE_PROPERTY} from '../constants.ts'; -describe.skip('RemoteElement', () => { +describe('RemoteElement', () => { describe('properties', () => { it('serializes initial properties declared with a static `remoteProperties` field', () => { class HelloElement extends RemoteElement { @@ -29,7 +29,7 @@ describe.skip('RemoteElement', () => { const element = new HelloElement(); element.name = name; - expect(receiver.receive).not.toHaveBeenCalled(); + expect(receiver.connection.mutate).not.toHaveBeenCalled(); root.append(element); @@ -59,7 +59,7 @@ describe.skip('RemoteElement', () => { const name = 'Winston'; element.name = name; - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', name], ]); }); @@ -74,7 +74,7 @@ describe.skip('RemoteElement', () => { const name = 'Winston'; element.name = name; - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', name], ]); }); @@ -92,7 +92,7 @@ describe.skip('RemoteElement', () => { const name = 'Winston'; element.name = name; - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', name], ]); }); @@ -108,7 +108,7 @@ describe.skip('RemoteElement', () => { const name = 'Winston'; element.name = name; - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', name], ]); }); @@ -125,7 +125,7 @@ describe.skip('RemoteElement', () => { const element = new HelloElement(); element.name = name; - expect(receiver.receive).not.toHaveBeenCalled(); + expect(receiver.connection.mutate).not.toHaveBeenCalled(); root.append(element); @@ -154,7 +154,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('name', name); expect(element.name).toBe(name); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', name], ]); }); @@ -171,7 +171,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('name', name); expect(element.name).toBe(name); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', name], ]); }); @@ -188,7 +188,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('name', name); expect(element.name).toBe(undefined); - expect(receiver.receive).not.toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).not.toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', name], ]); }); @@ -206,7 +206,7 @@ describe.skip('RemoteElement', () => { element.removeAttribute('name'); expect(element.name).toBe(undefined); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), 'name', undefined], ]); }); @@ -223,7 +223,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('updated-at', updatedAt); expect(element.updatedAt).toBe(updatedAt); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -246,7 +246,7 @@ describe.skip('RemoteElement', () => { element.setAttribute(attribute, updatedAt); expect(element.updatedAt).toBe(updatedAt); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -268,7 +268,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('inventory', String(inventory)); expect(element.inventory).toBe(inventory); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -280,7 +280,7 @@ describe.skip('RemoteElement', () => { element.removeAttribute('inventory'); expect(element.inventory).toBe(undefined); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -302,7 +302,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('collection', JSON.stringify(collection)); expect(element.collection).toStrictEqual(collection); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -314,7 +314,7 @@ describe.skip('RemoteElement', () => { element.removeAttribute('collection'); expect(element.collection).toBe(undefined); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -335,7 +335,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('collection', 'foo'); expect(element.collection).toBe(undefined); - expect(receiver.receive).not.toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).not.toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -359,7 +359,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('collections', JSON.stringify(collections)); expect(element.collections).toStrictEqual(collections); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -371,7 +371,7 @@ describe.skip('RemoteElement', () => { element.removeAttribute('collections'); expect(element.collections).toBe(undefined); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -394,7 +394,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('collections', 'foo'); expect(element.collections).toBe(undefined); - expect(receiver.receive).not.toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).not.toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -428,7 +428,7 @@ describe.skip('RemoteElement', () => { element.setAttribute('my-field', value); expect(element.myField).toBe(`${attributePrefix}${value}`); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -458,7 +458,7 @@ describe.skip('RemoteElement', () => { element.addEventListener('press', listener); expect(element.onPress).toBe(undefined); - expect(receiver.receive).not.toHaveBeenCalledWith([ + expect(receiver.connection.mutate).not.toHaveBeenCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, expect.anything(), @@ -480,7 +480,7 @@ describe.skip('RemoteElement', () => { element.addEventListener('press', listener); expect(element.onPress).toBeInstanceOf(Function); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -502,7 +502,7 @@ describe.skip('RemoteElement', () => { element.addEventListener('press', listener); expect(element.press).toBeInstanceOf(Function); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -524,7 +524,7 @@ describe.skip('RemoteElement', () => { element.addEventListener('click', listener); expect(element.onPress).toBeInstanceOf(Function); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -546,7 +546,7 @@ describe.skip('RemoteElement', () => { element.addEventListener('mouse-enter', listener); expect(element.onMouseEnter).toBeInstanceOf(Function); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -613,22 +613,22 @@ describe.skip('RemoteElement', () => { element.addEventListener('press', firstListener); - receiver.receive.mockClear(); + receiver.connection.mutate.mockClear(); element.addEventListener('press', secondListener); expect(element.onPress).toBeInstanceOf(Function); - expect(receiver.receive).not.toHaveBeenCalled(); + expect(receiver.connection.mutate).not.toHaveBeenCalled(); element.removeEventListener('press', secondListener); expect(element.onPress).toBeInstanceOf(Function); - expect(receiver.receive).not.toHaveBeenCalled(); + expect(receiver.connection.mutate).not.toHaveBeenCalled(); element.removeEventListener('press', firstListener); expect(element.onPress).toBeUndefined(); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -653,7 +653,7 @@ describe.skip('RemoteElement', () => { element.onPress(); expect(element.onPress).toBeUndefined(); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -679,7 +679,7 @@ describe.skip('RemoteElement', () => { abort.abort(); expect(element.onPress).toBeUndefined(); - expect(receiver.receive).toHaveBeenLastCalledWith([ + expect(receiver.connection.mutate).toHaveBeenLastCalledWith([ [ MUTATION_TYPE_UPDATE_PROPERTY, remoteId(element), @@ -693,17 +693,18 @@ describe.skip('RemoteElement', () => { }); class TestRemoteReceiver extends RemoteReceiver { - readonly receive: RemoteReceiver['receive'] & - Mock< - Parameters, - ReturnType - >; + readonly connection: RemoteReceiver['connection'] & + MockedObject; constructor() { super(); - // @ts-expect-error `this.receive` is defined on the superclass, - // but `super.receive` isn’t defined. Not sure how else to do this... - this.receive = vi.fn(this.receive); + this.connection = { + // @ts-expect-error `this.connection` is defined on the superclass, + // but `super.connection` isn’t defined. Not sure how else to do this... + mutate: vi.fn(this.connection.mutate), + // @ts-expect-error see above + call: vi.fn(this.connection.call), + }; } } @@ -723,6 +724,6 @@ function remoteId(node: any) { function createAndConnectRemoteRootElement() { const root = new RemoteRootElement() as RemoteRootElement; const receiver = new TestRemoteReceiver(); - root.connect(receiver.receive); + root.connect(receiver.connection); return {root, receiver}; } diff --git a/packages/core/source/types.ts b/packages/core/source/types.ts index d755c51a..ec4da4a5 100644 --- a/packages/core/source/types.ts +++ b/packages/core/source/types.ts @@ -43,9 +43,10 @@ export type RemoteMutationRecord = | RemoteMutationRecordUpdateText | RemoteMutationRecordUpdateProperty; -export type RemoteMutationCallback = ( - records: readonly RemoteMutationRecord[], -) => void | Promise; +export interface RemoteConnection { + mutate(records: readonly RemoteMutationRecord[]): void; + call(id: string, method: string, ...args: readonly unknown[]): unknown; +} export interface RemoteElementSerialization { readonly id: string; diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index ec6a79d4..6eb0a710 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -20,8 +20,7 @@ "types": "./build/typescript/index.d.ts", "quilt:source": "./source/index.ts", "quilt:esnext": "./build/esnext/index.esnext", - "import": "./build/esm/index.mjs", - "require": "./build/cjs/index.cjs" + "import": "./build/esm/index.mjs" } }, "types": "./build/typescript/index.d.ts", diff --git a/packages/preact/package.json b/packages/preact/package.json index 2f3b22c7..1ec9a02a 100644 --- a/packages/preact/package.json +++ b/packages/preact/package.json @@ -20,22 +20,19 @@ "types": "./build/typescript/index.d.ts", "quilt:source": "./source/index.ts", "quilt:esnext": "./build/esnext/index.esnext", - "import": "./build/esm/index.mjs", - "require": "./build/cjs/index.cjs" + "import": "./build/esm/index.mjs" }, "./host": { "types": "./build/typescript/host.d.ts", "quilt:source": "./source/host.ts", "quilt:esnext": "./build/esnext/host.esnext", - "import": "./build/esm/host.mjs", - "require": "./build/cjs/host.cjs" + "import": "./build/esm/host.mjs" }, "./html": { "types": "./build/typescript/html.d.ts", "quilt:source": "./source/html.ts", "quilt:esnext": "./build/esnext/html.esnext", - "import": "./build/esm/html.mjs", - "require": "./build/cjs/html.cjs" + "import": "./build/esm/html.mjs" } }, "types": "./build/typescript/index.d.ts", @@ -51,14 +48,15 @@ }, "sideEffects": false, "scripts": { - "build": "rollup --config ./rollup.config.js" + "build": "rollup --config rollup.config.js" }, "dependencies": { - "@remote-dom/core": "^0.0.1", - "@remote-dom/signals": "^0.0.1", + "@remote-dom/core": "workspace:^", + "@remote-dom/signals": "workspace:^", "htm": "^3.1.1" }, "peerDependencies": { + "@preact/signals": "^1.2.0", "@preact/signals-core": "^1.3.0", "preact": "^10.0.0" }, @@ -71,8 +69,10 @@ } }, "devDependencies": { + "@preact/signals": "^1.2.0", "@preact/signals-core": "^1.5.0", - "preact": "^10.17.0" + "@quilted/preact-testing": "^0.1.3", + "preact": "^10.19.0" }, "browserslist": [ "defaults and not dead" diff --git a/packages/preact/source/host.ts b/packages/preact/source/host.ts index f937e2b6..0843eb0f 100644 --- a/packages/preact/source/host.ts +++ b/packages/preact/source/host.ts @@ -13,10 +13,6 @@ export { type RemoteComponentRendererAdditionalProps, } from './host/component.tsx'; export {RemoteFragmentRenderer} from './host/RemoteFragmentRenderer.tsx'; -export { - RemoteTextRenderer, - type RemoteTextRendererProps, -} from './host/RemoteTextRenderer.tsx'; export { RemoteRootRenderer, type RemoteRootRendererProps, diff --git a/packages/preact/source/host/RemoteTextRenderer.tsx b/packages/preact/source/host/RemoteTextRenderer.tsx deleted file mode 100644 index 468695ed..00000000 --- a/packages/preact/source/host/RemoteTextRenderer.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { - SignalRemoteReceiver, - SignalRemoteReceiverText, -} from '@remote-dom/signals/receiver'; - -export interface RemoteTextRendererProps { - text: SignalRemoteReceiverText; - receiver: SignalRemoteReceiver; -} - -export function RemoteTextRenderer({text}: RemoteTextRendererProps) { - const data = text.data.value; - return data ? <>{data} : null; -} diff --git a/packages/preact/source/host/component.tsx b/packages/preact/source/host/component.tsx index e6cd98f0..d518435c 100644 --- a/packages/preact/source/host/component.tsx +++ b/packages/preact/source/host/component.tsx @@ -1,4 +1,5 @@ -import {forwardRef, type ForwardFn} from 'preact/compat'; +import type {ComponentType} from 'preact'; +import {memo, useRef, useEffect, type MutableRefObject} from 'preact/compat'; import type {RemoteReceiverElement} from '@remote-dom/core/receiver'; import {usePropsForRemoteElement} from './hooks/props-for-element.tsx'; @@ -9,17 +10,25 @@ export interface RemoteComponentRendererAdditionalProps { readonly [REMOTE_ELEMENT_PROP]: RemoteReceiverElement; } +interface Internals extends Pick { + id: string; + instanceRef: MutableRefObject; +} + export function createRemoteComponentRenderer< Props extends Record = {}, - Instance = never, >( - Component: ForwardFn, + Component: ComponentType, {name}: {name?: string} = {}, -): ReturnType> { - const RemoteComponentRenderer = forwardRef< - Instance, - RemoteComponentRendererProps - >(function RemoteComponentRenderer({element, receiver, components}, ref) { +): ComponentType { + const RemoteComponentRenderer = memo(function RemoteComponentRenderer({ + element, + receiver, + components, + }: RemoteComponentRendererProps) { + const internalsRef = useRef(); + + const {id} = element; const props = usePropsForRemoteElement(element, { receiver, components, @@ -27,7 +36,33 @@ export function createRemoteComponentRenderer< (props as any)[REMOTE_ELEMENT_PROP] = element; - return Component(props, ref); + if (internalsRef.current == null) { + const internals: Internals = { + id, + receiver, + } as any; + + internals.instanceRef = createImplementationRef(internals); + internalsRef.current = internals; + } + + internalsRef.current.id = id; + internalsRef.current.receiver = receiver; + + useEffect(() => { + const node = {id}; + + receiver.implement( + node, + internalsRef.current?.instanceRef.current as any, + ); + + return () => { + receiver.implement(node, null); + }; + }, [id, receiver]); + + return ; }); RemoteComponentRenderer.displayName = @@ -36,5 +71,21 @@ export function createRemoteComponentRenderer< Component.displayName ?? Component.name ?? 'Component' })`; - return RemoteComponentRenderer as any; + return RemoteComponentRenderer; +} + +function createImplementationRef( + internals: Pick, +): MutableRefObject { + let current: unknown = null; + + return { + get current() { + return current; + }, + set current(implementation) { + current = implementation; + internals.receiver.implement(internals, implementation as any); + }, + }; } diff --git a/packages/preact/source/host/node.tsx b/packages/preact/source/host/node.tsx index 8cae2b6e..4a034e57 100644 --- a/packages/preact/source/host/node.tsx +++ b/packages/preact/source/host/node.tsx @@ -8,7 +8,6 @@ import type { SignalRemoteReceiverNode, } from '@remote-dom/signals/receiver'; -import {RemoteTextRenderer} from './RemoteTextRenderer.tsx'; import type {RemoteComponentRendererMap} from './types.ts'; export interface RenderRemoteNodeOptions { @@ -40,9 +39,7 @@ export function renderRemoteNode( ); } case NODE_TYPE_TEXT: { - return ( - - ); + return node.data; } case NODE_TYPE_COMMENT: { return null; diff --git a/packages/preact/source/tests/e2e.test.tsx b/packages/preact/source/tests/e2e.test.tsx new file mode 100644 index 00000000..8ae07bdf --- /dev/null +++ b/packages/preact/source/tests/e2e.test.tsx @@ -0,0 +1,219 @@ +// @vitest-environment jsdom + +import {describe, it, expect, vi} from 'vitest'; + +import {render as preactRender} from 'preact'; +import { + useState, + useRef, + useImperativeHandle, + forwardRef, + type PropsWithChildren, +} from 'preact/compat'; + +// The `SignalRemoteReceiver` library uses `@preact/signals-core`, which does not include +// the auto-updating of Preact components when they use signals. Importing this library +// applies the internal hooks that make this work. +import '@preact/signals'; + +import {render} from '@quilted/preact-testing'; +import {matchers, type CustomMatchers} from '@quilted/preact-testing/matchers'; + +import { + RemoteMutationObserver, + createRemoteElement, +} from '@remote-dom/core/elements'; +import {SignalRemoteReceiver} from '@remote-dom/signals/receiver'; + +import {createRemoteComponent} from '../index.ts'; +import {RemoteRootRenderer, createRemoteComponentRenderer} from '../host.ts'; + +expect.extend(matchers); + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} + +interface ButtonProps { + disabled?: boolean; + onPress?(): void; +} + +const HostButton = forwardRef(function HostButton( + {children, disabled, onPress}: PropsWithChildren, + ref, +) { + const buttonRef = useRef(null); + + useImperativeHandle(ref, () => ({ + focus() { + buttonRef.current?.focus(); + }, + })); + + return ( + + ); +}); + +const RemoteButtonElement = createRemoteElement({ + properties: { + disabled: {type: Boolean}, + onPress: {type: Function}, + }, + methods: ['focus'], +}); + +customElements.define('remote-button', RemoteButtonElement); + +const RemoteButton = createRemoteComponent( + 'remote-button', + RemoteButtonElement, +); + +const components = new Map([ + ['remote-button', createRemoteComponentRenderer(HostButton)], +]); + +declare global { + interface HTMLElementTagNameMap { + 'remote-button': InstanceType; + } +} + +describe('preact', () => { + it('can render simple remote DOM elements', async () => { + const receiver = new SignalRemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.textContent = 'Click me!'; + remoteRoot.append(remoteButton); + + const rendered = render( + , + ); + + expect(rendered).not.toContainPreactComponent(HostButton); + + rendered.act(() => { + mutationObserver.observe(remoteRoot); + }); + + expect(rendered).toContainPreactComponent(HostButton); + }); + + it('can render remote DOM elements with simple properties', async () => { + const receiver = new SignalRemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.setAttribute('disabled', ''); + remoteButton.textContent = 'Disabled button'; + remoteRoot.append(remoteButton); + mutationObserver.observe(remoteRoot); + + const rendered = render( + , + ); + + expect(rendered).toContainPreactComponent(HostButton, {disabled: true}); + }); + + it('can render remote DOM elements with event listeners', async () => { + const receiver = new SignalRemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.textContent = 'Click to disable'; + + remoteButton.addEventListener( + 'press', + () => { + remoteButton.textContent = 'Already disabled'; + remoteButton.setAttribute('disabled', ''); + }, + {once: true}, + ); + + remoteRoot.append(remoteButton); + mutationObserver.observe(remoteRoot); + + const rendered = render( + , + ); + + expect(rendered).toContainPreactComponent(HostButton, {disabled: false}); + + rendered.find(HostButton)?.trigger('onPress'); + + expect(rendered).toContainPreactComponent(HostButton, {disabled: true}); + }); + + it('can call methods on a remote DOM element by forwarding calls to the host’s implementation component ref', async () => { + const receiver = new SignalRemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.onPress = () => { + remoteButton.focus(); + }; + remoteRoot.append(remoteButton); + mutationObserver.observe(remoteRoot); + + const rendered = render( + , + ); + + const focusSpy = vi.spyOn(rendered.find(HostButton)!.domNode!, 'focus'); + + rendered.find(HostButton)?.trigger('onPress'); + + expect(focusSpy).toHaveBeenCalled(); + }); + + it('can render remote DOM elements wrapped as Preact components', async () => { + const receiver = new SignalRemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + + function Remote() { + const [disabled, setDisabled] = useState(false); + + return ( + { + setDisabled(true); + }} + > + {disabled ? 'Already disabled' : 'Click to disable'} + + ); + } + + preactRender(, remoteRoot); + + const rendered = render( + , + ); + + rendered.act(() => { + mutationObserver.observe(remoteRoot); + }); + + expect(rendered).toContainPreactComponent(HostButton, {disabled: false}); + + rendered.find(HostButton)?.trigger('onPress'); + + expect(rendered).toContainPreactComponent(HostButton, {disabled: true}); + }); +}); diff --git a/packages/preact/vite.config.js b/packages/preact/vite.config.js new file mode 100644 index 00000000..bb8dad83 --- /dev/null +++ b/packages/preact/vite.config.js @@ -0,0 +1,22 @@ +import {defineConfig} from 'vitest/config'; +import {quiltPackage} from '@quilted/vite/package'; + +export default defineConfig({ + plugins: [quiltPackage({react: 'preact'})], + test: { + deps: { + optimizer: { + web: { + // Without this, some imports for Preact get the node_modules version, and others get + // the optimized dependency version. + exclude: [ + 'preact', + 'preact/compat', + '@preact/signals', + '@preact/signals-core', + ], + }, + }, + }, + }, +}); diff --git a/packages/react/package.json b/packages/react/package.json index 302dfd17..f8385730 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -8,7 +8,7 @@ }, "version": "0.0.1", "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "repository": { "type": "git", @@ -20,29 +20,25 @@ "types": "./build/typescript/index.d.ts", "quilt:source": "./source/index.ts", "quilt:esnext": "./build/esnext/index.esnext", - "import": "./build/esm/index.mjs", - "require": "./build/cjs/index.cjs" + "import": "./build/esm/index.mjs" }, "./host": { "types": "./build/typescript/host.d.ts", "quilt:source": "./source/host.ts", "quilt:esnext": "./build/esnext/host.esnext", - "import": "./build/esm/host.mjs", - "require": "./build/cjs/host.cjs" + "import": "./build/esm/host.mjs" }, "./html": { "types": "./build/typescript/html.d.ts", "quilt:source": "./source/html.ts", "quilt:esnext": "./build/esnext/html.esnext", - "import": "./build/esm/html.mjs", - "require": "./build/cjs/html.cjs" + "import": "./build/esm/html.mjs" }, "./polyfill": { "types": "./build/typescript/polyfill.d.ts", "quilt:source": "./source/polyfill.ts", "quilt:esnext": "./build/esnext/polyfill.esnext", - "import": "./build/esm/polyfill.mjs", - "require": "./build/cjs/polyfill.cjs" + "import": "./build/esm/polyfill.mjs" } }, "types": "./build/typescript/index.d.ts", @@ -66,11 +62,11 @@ "./build/esnext/polyfill.esnext" ], "scripts": { - "build": "rollup --config ./rollup.config.js" + "build": "rollup --config rollup.config.js" }, "dependencies": { - "@remote-dom/core": "^0.0.1", - "@types/react": "^17.0.0 || ^18.0.0", + "@remote-dom/core": "workspace:^", + "@types/react": "^18.0.0", "htm": "^3.1.1" }, "peerDependencies": { @@ -82,8 +78,11 @@ } }, "devDependencies": { - "@quilted/testing": "^0.1.6", - "react": "npm:@quilted/react@^18.2.0" + "@quilted/react-testing": "^0.6.5", + "@types/react-dom": "^18.2.0", + "preact": "^10.19.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "browserslist": [ "defaults and not dead" diff --git a/packages/react/source/host/component.tsx b/packages/react/source/host/component.tsx index 13501b96..5c3de07c 100644 --- a/packages/react/source/host/component.tsx +++ b/packages/react/source/host/component.tsx @@ -1,8 +1,10 @@ import type {RemoteReceiverElement} from '@remote-dom/core/receiver'; import { - forwardRef, - type ForwardRefRenderFunction, - type ForwardRefExoticComponent, + memo, + useRef, + useEffect, + type MutableRefObject, + type ComponentType, } from 'react'; import {useRemoteReceived} from './hooks/remote-received.ts'; @@ -18,19 +20,28 @@ export interface RemoteComponentRendererAdditionalProps { readonly [REMOTE_ELEMENT_ATTACHED_PROP]: boolean; } +interface Internals extends Pick { + id: string; + instanceRef: MutableRefObject; +} + export function createRemoteComponentRenderer< Props extends Record = {}, - Instance = never, >( - Component: ForwardRefRenderFunction, + Component: ComponentType, {name}: {name?: string} = {}, -): ForwardRefExoticComponent { - const RemoteComponentRenderer = forwardRef< - Instance, - RemoteComponentRendererProps - >(function RemoteComponentRenderer({element, receiver, components}, ref) { +): ComponentType { + const RemoteComponentRenderer = memo(function RemoteComponentRenderer({ + element, + receiver, + components, + }: RemoteComponentRendererProps) { + const internalsRef = useRef(); + const attachedElement = useRemoteReceived(element, receiver); const resolvedElement = attachedElement ?? element; + const resolvedId = resolvedElement.id; + const props = usePropsForRemoteElement(resolvedElement, { receiver, components, @@ -39,7 +50,33 @@ export function createRemoteComponentRenderer< (props as any)[REMOTE_ELEMENT_PROP] = resolvedElement; (props as any)[REMOTE_ELEMENT_ATTACHED_PROP] = attachedElement != null; - return Component(props, ref); + if (internalsRef.current == null) { + const internals: Internals = { + id: resolvedId, + receiver, + } as any; + + internals.instanceRef = createImplementationRef(internals); + internalsRef.current = internals; + } + + internalsRef.current.id = resolvedId; + internalsRef.current.receiver = receiver; + + useEffect(() => { + const node = {id: resolvedId}; + + receiver.implement( + node, + internalsRef.current?.instanceRef.current as any, + ); + + return () => { + receiver.implement(node, null); + }; + }, [resolvedId, receiver]); + + return ; }); RemoteComponentRenderer.displayName = @@ -50,3 +87,19 @@ export function createRemoteComponentRenderer< return RemoteComponentRenderer; } + +function createImplementationRef( + internals: Pick, +): MutableRefObject { + let current: unknown = null; + + return { + get current() { + return current; + }, + set current(implementation) { + current = implementation; + internals.receiver.implement(internals, implementation as any); + }, + }; +} diff --git a/packages/react/source/tests/e2e.test.tsx b/packages/react/source/tests/e2e.test.tsx new file mode 100644 index 00000000..5b1bcd91 --- /dev/null +++ b/packages/react/source/tests/e2e.test.tsx @@ -0,0 +1,220 @@ +// @vitest-environment jsdom + +Object.assign(globalThis, {IS_REACT_ACT_ENVIRONMENT: true}); + +import {describe, it, expect, vi} from 'vitest'; + +import {createRoot} from 'react-dom/client'; +import { + useState, + useRef, + useImperativeHandle, + forwardRef, + type PropsWithChildren, +} from 'react'; + +import {render} from '@quilted/react-testing/dom'; +import {matchers, type CustomMatchers} from '@quilted/react-testing/matchers'; + +import { + RemoteMutationObserver, + createRemoteElement, +} from '@remote-dom/core/elements'; +import {RemoteReceiver} from '@remote-dom/core/receiver'; + +import {createRemoteComponent} from '../index.ts'; +import {RemoteRootRenderer, createRemoteComponentRenderer} from '../host.ts'; + +expect.extend(matchers); + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} + +interface ButtonProps { + disabled?: boolean; + onPress?(): void; +} + +const HostButton = forwardRef(function HostButton( + {children, disabled, onPress}: PropsWithChildren, + ref, +) { + const buttonRef = useRef(null); + + useImperativeHandle(ref, () => ({ + focus() { + buttonRef.current?.focus(); + }, + })); + + return ( + + ); +}); + +const RemoteButtonElement = createRemoteElement({ + properties: { + disabled: {type: Boolean}, + onPress: {type: Function}, + }, + methods: ['focus'], +}); + +customElements.define('remote-button', RemoteButtonElement); + +const RemoteButton = createRemoteComponent( + 'remote-button', + RemoteButtonElement, +); + +const components = new Map([ + ['remote-button', createRemoteComponentRenderer(HostButton)], +]); + +declare global { + interface HTMLElementTagNameMap { + 'remote-button': InstanceType; + } +} + +describe('react', () => { + it('can render simple remote DOM elements', async () => { + const receiver = new RemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.textContent = 'Click me!'; + remoteRoot.append(remoteButton); + + const rendered = render( + , + ); + + expect(rendered).not.toContainReactComponent(HostButton); + + rendered.act(() => { + mutationObserver.observe(remoteRoot); + }); + + expect(rendered).toContainReactComponent(HostButton); + }); + + it('can render remote DOM elements with simple properties', async () => { + const receiver = new RemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.setAttribute('disabled', ''); + remoteButton.textContent = 'Disabled button'; + remoteRoot.append(remoteButton); + mutationObserver.observe(remoteRoot); + + const rendered = render( + , + ); + + expect(rendered).toContainReactComponent(HostButton, {disabled: true}); + }); + + it('can render remote DOM elements with event listeners', async () => { + const receiver = new RemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.textContent = 'Click to disable'; + + remoteButton.addEventListener( + 'press', + () => { + remoteButton.textContent = 'Already disabled'; + remoteButton.setAttribute('disabled', ''); + }, + {once: true}, + ); + + remoteRoot.append(remoteButton); + mutationObserver.observe(remoteRoot); + + const rendered = render( + , + ); + + expect(rendered).toContainReactComponent(HostButton, {disabled: false}); + + rendered.find(HostButton)?.trigger('onPress'); + + expect(rendered).toContainReactComponent(HostButton, {disabled: true}); + }); + + it('can call methods on a remote DOM element by forwarding calls to the host’s implementation component ref', async () => { + const receiver = new RemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + const remoteButton = document.createElement('remote-button'); + remoteButton.onPress = () => { + remoteButton.focus(); + }; + remoteRoot.append(remoteButton); + mutationObserver.observe(remoteRoot); + + const rendered = render( + , + ); + + const focusSpy = vi.spyOn(rendered.find(HostButton)!.domNode!, 'focus'); + + rendered.find(HostButton)?.trigger('onPress'); + + expect(focusSpy).toHaveBeenCalled(); + }); + + it('can render remote DOM elements wrapped as React components', async () => { + const receiver = new RemoteReceiver(); + const mutationObserver = new RemoteMutationObserver(receiver.connection); + + const remoteRoot = document.createElement('div'); + + function Remote() { + const [disabled, setDisabled] = useState(false); + + return ( + { + setDisabled(true); + }} + > + {disabled ? 'Already disabled' : 'Click to disable'} + + ); + } + + const rendered = render( + , + ); + + // Dedicated `act()` so that React actually renders the remote elements, + // before we start observing the remote DOM node for changes. + rendered.act(() => { + createRoot(remoteRoot).render(); + }); + + rendered.act(() => { + mutationObserver.observe(remoteRoot); + }); + + expect(rendered).toContainReactComponent(HostButton, {disabled: false}); + + rendered.find(HostButton)?.trigger('onPress'); + + expect(rendered).toContainReactComponent(HostButton, {disabled: true}); + }); +}); diff --git a/packages/react/vite.config.js b/packages/react/vite.config.js new file mode 100644 index 00000000..2e0a1a17 --- /dev/null +++ b/packages/react/vite.config.js @@ -0,0 +1,21 @@ +import {defineConfig} from 'vitest/config'; +import {quiltPackage} from '@quilted/vite/package'; + +export default defineConfig({ + plugins: [ + quiltPackage({ + react: 'react', + }), + ], + test: { + deps: { + optimizer: { + web: { + // Without this, some imports for React get the node_modules version, and others get + // the optimized dependency version. + exclude: ['react', 'react-dom'], + }, + }, + }, + }, +}); diff --git a/packages/signals/package.json b/packages/signals/package.json index 6d82f23d..0859fc86 100644 --- a/packages/signals/package.json +++ b/packages/signals/package.json @@ -20,15 +20,13 @@ "types": "./build/typescript/index.d.ts", "quilt:source": "./source/index.ts", "quilt:esnext": "./build/esnext/index.esnext", - "import": "./build/esm/index.mjs", - "require": "./build/cjs/index.cjs" + "import": "./build/esm/index.mjs" }, "./receiver": { "types": "./build/typescript/receiver.d.ts", "quilt:source": "./source/receiver.ts", "quilt:esnext": "./build/esnext/receiver.esnext", - "import": "./build/esm/receiver.mjs", - "require": "./build/cjs/receiver.cjs" + "import": "./build/esm/receiver.mjs" } }, "types": "./build/typescript/index.d.ts", @@ -44,7 +42,7 @@ }, "peerDependencies": { "@preact/signals-core": "^1.3.0", - "@remote-dom/core": "^0.0.1" + "@remote-dom/core": "workspace:^" }, "peerDependenciesMeta": { "@preact/signals-core": { @@ -55,7 +53,7 @@ } }, "devDependencies": { - "@remote-dom/core": "^0.0.1", + "@remote-dom/core": "workspace:^", "@preact/signals-core": "^1.5.0" }, "browserslist": [ diff --git a/packages/signals/source/receiver.ts b/packages/signals/source/receiver.ts index 9dffafb1..f303cd24 100644 --- a/packages/signals/source/receiver.ts +++ b/packages/signals/source/receiver.ts @@ -6,8 +6,8 @@ import { NODE_TYPE_ELEMENT, NODE_TYPE_COMMENT, NODE_TYPE_TEXT, - createRemoteMutationCallback, - type RemoteMutationCallback, + createRemoteConnection, + type RemoteConnection, type RemoteNodeSerialization, type RemoteTextSerialization, type RemoteCommentSerialization, @@ -63,17 +63,29 @@ export class SignalRemoteReceiver { >([[ROOT_ID, this.root]]); private readonly parents = new Map(); + private readonly implementations = new Map< + string, + Record unknown> + >(); - readonly receive: RemoteMutationCallback; - - get callback() { - return this.receive; - } + readonly connection: RemoteConnection; constructor({retain, release}: RemoteReceiverOptions = {}) { const {attached, parents} = this; - this.receive = createRemoteMutationCallback({ + this.connection = createRemoteConnection({ + call: (id, method, ...args) => { + const implementation = this.implementations.get(id); + const implementationMethod = implementation?.[method]; + + if (typeof implementationMethod !== 'function') { + throw new Error( + `Node ${id} does not implement the ${method}() method`, + ); + } + + return implementationMethod(...args); + }, insertChild: (id, child, index) => { const parent = attached.get(id) as SignalRemoteReceiverParent; const newChildren = [...parent.children.peek()]; @@ -203,6 +215,17 @@ export class SignalRemoteReceiver { } } + implement( + {id}: Pick, + implementation?: Record unknown> | null, + ) { + if (implementation == null) { + this.implementations.delete(id); + } else { + this.implementations.set(id, implementation); + } + } + get({ id, }: Pick): T | undefined { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 245fc026..43e2a2f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@types/node': specifier: ~20.9.0 version: 20.9.0 + jsdom: + specifier: ^23.0.1 + version: 23.0.1 prettier: specifier: ^3.1.0 version: 3.1.1 @@ -43,7 +46,7 @@ importers: version: 5.0.10(@types/node@20.9.0) vitest: specifier: ^1.1.1 - version: 1.1.1(@types/node@20.9.0) + version: 1.1.1(@types/node@20.9.0)(jsdom@23.0.1) examples/kitchen-sink-vite: dependencies: @@ -115,40 +118,55 @@ importers: packages/preact: dependencies: '@remote-dom/core': - specifier: ^0.0.1 + specifier: workspace:^ version: link:../core '@remote-dom/signals': - specifier: ^0.0.1 + specifier: workspace:^ version: link:../signals htm: specifier: ^3.1.1 version: 3.1.1 devDependencies: + '@preact/signals': + specifier: ^1.2.0 + version: 1.2.2(preact@10.19.3) '@preact/signals-core': specifier: ^1.5.0 version: 1.5.0 + '@quilted/preact-testing': + specifier: ^0.1.3 + version: 0.1.3(preact@10.19.3) preact: - specifier: ^10.17.0 - version: 10.17.1 + specifier: ^10.19.0 + version: 10.19.3 packages/react: dependencies: '@remote-dom/core': - specifier: ^0.0.1 + specifier: workspace:^ version: link:../core '@types/react': - specifier: ^17.0.0 || ^18.0.0 + specifier: ^18.0.0 version: 18.2.22 htm: specifier: ^3.1.1 version: 3.1.1 devDependencies: - '@quilted/testing': - specifier: ^0.1.6 - version: 0.1.6 + '@quilted/react-testing': + specifier: ^0.6.5 + version: 0.6.5(preact@10.19.3)(react-dom@18.2.0)(react@18.2.0) + '@types/react-dom': + specifier: ^18.2.0 + version: 18.2.18 + preact: + specifier: ^10.19.0 + version: 10.19.3 react: - specifier: npm:@quilted/react@^18.2.0 - version: /@quilted/react@18.2.5 + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) packages/signals: devDependencies: @@ -156,7 +174,7 @@ importers: specifier: ^1.5.0 version: 1.5.0 '@remote-dom/core': - specifier: ^0.0.1 + specifier: workspace:^ version: link:../core packages: @@ -2181,6 +2199,15 @@ packages: resolution: {integrity: sha512-U2diO1Z4i1n2IoFgMYmRdHWGObNrcuTRxyNEn7deSq2cru0vj0583HYQZHsAqcs7FE+hQyX3mjIV7LAfHCvy8w==} dev: true + /@preact/signals@1.2.2(preact@10.19.3): + resolution: {integrity: sha512-ColCqdo4cRP18bAuIR4Oik5rDpiyFtPIJIygaYPMEAwTnl4buWkBOflGBSzhYyPyJfKpkwlekrvK+1pzQ2ldWw==} + peerDependencies: + preact: 10.x + dependencies: + '@preact/signals-core': 1.5.0 + preact: 10.19.3 + dev: true + /@prefresh/babel-plugin@0.5.1: resolution: {integrity: sha512-uG3jGEAysxWoyG3XkYfjYHgaySFrSsaEb4GagLzYaxlydbuREtaX+FTxuIidp241RaLl85XoHg9Ej6E4+V1pcg==} @@ -2251,13 +2278,42 @@ packages: resolution: {integrity: sha512-60CNw2V/WJjEsShHaSSGEfNsa3yAGfZYsUACgdPpR0Pypd9PUF1doUhTvTyY09A0nJ/9WxEHWoDV6p2YFGtu2w==} dev: true - /@quilted/react@18.2.5: - resolution: {integrity: sha512-kf7LoicZDiqXQ2jwaVKSj56fZWQTDnUNrq4rgJqW5xoSLNEfHALQopSQpTVNkhzwuelUo+7DziWQQXYCwtoagA==} + /@quilted/preact-testing@0.1.3(preact@10.19.3): + resolution: {integrity: sha512-OVF16bV1evaahoWkoslJHA9pTuUEB2rcztJveoW3EYgXWldgITHf2zKLlLCDKUBxkt5HJEfSsI9USw5P+A2LFA==} + peerDependencies: + preact: ^10.19.0 + peerDependenciesMeta: + preact: + optional: true dependencies: - '@types/react': 18.2.22 + jest-matcher-utils: 27.5.1 preact: 10.19.3 dev: true + /@quilted/react-testing@0.6.5(preact@10.19.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/VZdpTlYjIzueFCSsP/owGORZWcLl6c1Ur98IllqceJDVqtRekVQtEj0WuWqByt6+CPtcVye5bAxfb8mEF+12g==} + peerDependencies: + preact: ^10.19.0 + react: ^18.0.0 + react-dom: ^18.0.0 + react-test-renderer: ^18.0.0 + peerDependenciesMeta: + preact: + optional: true + react: + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + dependencies: + '@quilted/preact-testing': 0.1.3(preact@10.19.3) + jest-matcher-utils: 27.5.1 + preact: 10.19.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + /@quilted/request-router@0.2.0: resolution: {integrity: sha512-quwOFCI1LTNhyWNQ6ZUKNCG9tUIBvGJptMXfbDYgBPaO/VQiz81lcjkkIVkgoRC4Eb0o5qvoJbMGw4aEHV2x4Q==} dependencies: @@ -2365,7 +2421,7 @@ packages: '@quilted/request-router': 0.2.0 '@quilted/rollup': 0.2.20(@babel/template@7.22.15)(rollup@4.9.2) vite: 5.0.10(@types/node@20.9.0) - vitest: 1.1.1(@types/node@20.9.0) + vitest: 1.1.1(@types/node@20.9.0)(jsdom@23.0.1) transitivePeerDependencies: - '@babel/template' - '@babel/traverse' @@ -2661,6 +2717,12 @@ packages: /@types/prop-types@15.7.6: resolution: {integrity: sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg==} + /@types/react-dom@18.2.18: + resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==} + dependencies: + '@types/react': 18.2.22 + dev: true + /@types/react@18.2.22: resolution: {integrity: sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==} dependencies: @@ -2747,6 +2809,15 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + /agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -2852,6 +2923,10 @@ packages: lodash: 4.17.21 dev: true + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -3134,6 +3209,13 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true @@ -3192,6 +3274,13 @@ packages: engines: {node: '>= 6'} dev: false + /cssstyle@3.0.0: + resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} + engines: {node: '>=14'} + dependencies: + rrweb-cssom: 0.6.0 + dev: true + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} @@ -3217,6 +3306,14 @@ packages: stream-transform: 2.1.3 dev: true + /data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + dev: true + /dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} dev: true @@ -3267,6 +3364,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: true + /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -3308,6 +3409,11 @@ packages: object-keys: 1.1.1 dev: true + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + /depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -3437,7 +3543,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: false /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -3688,6 +3793,15 @@ packages: signal-exit: 4.1.0 dev: true + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + /fresh@0.5.2: resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=} engines: {node: '>= 0.6'} @@ -3918,6 +4032,13 @@ packages: whatwg-encoding: 2.0.0 dev: true + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 + dev: true + /http-errors@1.8.1: resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} engines: {node: '>= 0.6'} @@ -3929,6 +4050,16 @@ packages: toidentifier: 1.0.1 dev: true + /http-proxy-agent@7.0.0: + resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -3963,6 +4094,16 @@ packages: - supports-color dev: true + /https-proxy-agent@7.0.2: + resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} dev: true @@ -4122,6 +4263,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: @@ -4252,6 +4397,42 @@ packages: esprima: 4.0.1 dev: true + /jsdom@23.0.1: + resolution: {integrity: sha512-2i27vgvlUsGEBO9+/kJQRbtqtm+191b5zAZrU/UezVmnC2dlDAFLgDYJvAEi94T4kjsRKkezEtLQTgsNEsW2lQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + cssstyle: 3.0.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.2 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.16.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -4472,7 +4653,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: false /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -4567,6 +4747,18 @@ packages: picomatch: 2.3.1 dev: true + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -4704,6 +4896,10 @@ packages: boolbase: 1.0.0 dev: false + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: true + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true @@ -4828,6 +5024,12 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4921,10 +5123,6 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 - /preact@10.17.1: - resolution: {integrity: sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA==} - dev: true - /preact@10.19.3: resolution: {integrity: sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==} @@ -4972,6 +5170,15 @@ packages: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: true + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + /qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -4979,6 +5186,10 @@ packages: side-channel: 1.0.4 dev: true + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -5001,7 +5212,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -5021,7 +5231,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -5222,6 +5431,10 @@ packages: '@rollup/rollup-win32-x64-msvc': 4.9.2 fsevents: 2.3.3 + /rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -5254,11 +5467,17 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /secure-compare@3.0.1: resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} @@ -5564,6 +5783,10 @@ packages: periscopic: 3.1.0 dev: false + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + /systemjs@6.14.2: resolution: {integrity: sha512-1TlOwvKWdXxAY9vba+huLu99zrQURDWA8pUTYsRIYDZYQbGyK+pyEP4h4dlySsqo7ozyJBmYD20F+iUHhAltEg==} dev: true @@ -5610,10 +5833,27 @@ packages: engines: {node: '>=0.6'} dev: true + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + dependencies: + punycode: 2.3.1 + dev: true + /trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -5764,6 +6004,11 @@ packages: engines: {node: '>= 4.0.0'} dev: true + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + /update-browserslist-db@1.0.12(browserslist@4.21.10): resolution: {integrity: sha512-tE1smlR58jxbFMtrMpFNRmsrOXlpNXss965T1CrpwuZUzUAg/TBQc94SpyhDLSzrqrJS9xTRBthnZAGcE1oaxg==} hasBin: true @@ -5799,6 +6044,13 @@ packages: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} dev: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -5862,7 +6114,7 @@ packages: optionalDependencies: fsevents: 2.3.3 - /vitest@1.1.1(@types/node@20.9.0): + /vitest@1.1.1(@types/node@20.9.0)(jsdom@23.0.1): resolution: {integrity: sha512-Ry2qs4UOu/KjpXVfOCfQkTnwSXYGrqTbBZxw6reIYEFjSy1QUARRg5pxiI5BEXy+kBVntxUYNMlq4Co+2vD3fQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -5898,6 +6150,7 @@ packages: chai: 4.3.10 debug: 4.3.4 execa: 8.0.1 + jsdom: 23.0.1 local-pkg: 0.5.0 magic-string: 0.30.5 pathe: 1.1.1 @@ -5919,6 +6172,13 @@ packages: - terser dev: true + /w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + dependencies: + xml-name-validator: 5.0.0 + dev: true + /wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: @@ -5929,6 +6189,11 @@ packages: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -5936,6 +6201,26 @@ packages: iconv-lite: 0.6.3 dev: true + /whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + dev: true + + /whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + dev: true + /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -6031,6 +6316,28 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} dev: true