Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
10208cf
Initial legacy adaptors
lemonmade Aug 15, 2024
e9f0c8e
Cleanup and documentation
lemonmade Aug 15, 2024
1e3cac1
Add remote root adaptor
lemonmade Aug 26, 2024
51bd6b5
Add fragment support
igor10k Jan 7, 2025
6216542
Add remote-ui example
robin-drexler Nov 24, 2024
3d33cd7
E2E tests for legacy adapter
igor10k Jan 7, 2025
026fd14
changeset
lemonmade Aug 26, 2024
4f22007
Delete legacy/remote
igor10k Jan 7, 2025
b83a127
remove unused file
robin-drexler Jan 9, 2025
7cf9442
adjust remote ui example title
robin-drexler Jan 10, 2025
9eced78
remove unused wait-on dep
robin-drexler Jan 10, 2025
13efd1c
remove superfluous ts references
robin-drexler Jan 10, 2025
27d47ba
Update packages/core/README.md
igor10k Jan 10, 2025
2f93969
move remote-ui example into kitchen sink
robin-drexler Jan 10, 2025
8933a30
Update changeset copy
igor10k Jan 10, 2025
7f9689a
Change the order of functions
igor10k Jan 10, 2025
41a30eb
Switch tree to Map
igor10k Jan 10, 2025
d771f15
Get rid of negation in conditions
igor10k Jan 10, 2025
656152e
Lift out "update props" variables
igor10k Jan 10, 2025
53c1b2e
Add slotWrapper
igor10k Jan 10, 2025
19f7240
Update packages/core/source/legacy/host.ts
igor10k Jan 13, 2025
410ad7c
slotWrapper -> slotProps.wrapper
igor10k Jan 13, 2025
587f994
Add first set of tests
simontaisne Jan 13, 2025
4a2ab2e
Update tests
simontaisne Jan 14, 2025
9e4f271
Fix test imports
simontaisne Jan 14, 2025
56eab19
Add additional tests
simontaisne Jan 15, 2025
c409934
Extract adapter into a separate package
igor10k Jan 15, 2025
28c82ad
Use adapter component mapping
igor10k Jan 15, 2025
2b839d0
ensure files are imported from source
robin-drexler Jan 15, 2025
0e2ab31
renderRemoteUi -> renderLegacy
igor10k Jan 16, 2025
d1ad8d4
Merge imports
igor10k Jan 16, 2025
df77bf3
compat v0.1.0
igor10k Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/beige-windows-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@remote-dom/core': minor
'@remote-dom/polyfill': minor
'@remote-dom/preact': minor
'@remote-dom/react': minor
'@remote-dom/signals': minor
---

Add a `adaptToLegacyRemoteChannel` helper that adapts a Remote DOM `RemoteConnection` object into a `remote-ui` `RemoteChannel`.

It allows to use a Remote DOM receiver class on the host, even if the remote environment is using `remote-ui`.
5 changes: 4 additions & 1 deletion e2e/basic.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {test, expect} from '@playwright/test';
['iframe', 'vue'],
['iframe', 'htm'],
['iframe', 'react'],
['iframe', 'react-remote-ui'],
['worker', 'vanilla'],
['worker', 'preact'],
['worker', 'svelte'],
// ['worker', 'vue'],
['worker', 'htm'],
['worker', 'react'],
['worker', 'react-remote-ui'],
].forEach(([sandbox, example]) => {
test(`basic modal interaction with ${sandbox} sandbox and ${example} example`, async ({
page,
Expand All @@ -30,7 +32,8 @@ import {test, expect} from '@playwright/test';
dialog.dismiss().catch(() => {});
});

const dialogPromise = page.waitForEvent('dialog');
await page.getByRole('button', {name: 'Close'}).click();
await page.waitForEvent('dialog');
await dialogPromise;
});
});
46 changes: 27 additions & 19 deletions examples/kitchen-sink/app/host.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {ThreadIframe, ThreadWebWorker} from '@quilted/threads';
import type {SandboxAPI} from './types.ts';
import {Button, Modal, Stack, Text, ControlPanel} from './host/components.tsx';
import {createState} from './host/state.ts';
import {adaptToLegacyRemoteChannel} from '@remote-dom/core/legacy';

// We will put any remote elements we want to render in this root element.
const uiRoot = document.querySelector('main')!;
Expand Down Expand Up @@ -42,6 +43,10 @@ const components = new Map([
['ui-button', createRemoteComponentRenderer(Button)],
['ui-stack', createRemoteComponentRenderer(Stack)],
['ui-modal', createRemoteComponentRenderer(Modal)],
['Text', createRemoteComponentRenderer(Text)],
['Button', createRemoteComponentRenderer(Button)],
['Stack', createRemoteComponentRenderer(Stack)],
['Modal', createRemoteComponentRenderer(Modal)],
// The `remote-fragment` element is a special element created by Remote DOM when
// it needs an unstyled container for a list of elements. This is primarily used
// to convert elements passed as a prop to React or Preact components into a slotted
Expand All @@ -59,27 +64,30 @@ const components = new Map([

const {receiver, example, sandbox} = createState(
async ({receiver, example, sandbox}) => {
if (sandbox === 'iframe') {
await iframeSandbox.imports.render(receiver.connection, {
sandbox,
example,
async alert(content) {
console.log(
`Alert API used by example ${example} in the iframe sandbox`,
);
window.alert(content);
},
const api = {
sandbox,
example,
async alert(content: string) {
console.log(
`Alert API used by example ${example} in the iframe sandbox`,
);
window.alert(content);
},
async closeModal() {
document.querySelector('dialog')?.close();
},
};

const sandboxToUse = sandbox === 'iframe' ? iframeSandbox : workerSandbox;

if (example === 'react-remote-ui') {
const remoteUiChannel = adaptToLegacyRemoteChannel(receiver.connection);
await sandboxToUse.imports.renderRemoteUi(remoteUiChannel, {
...api,
});
} else {
await workerSandbox.imports.render(receiver.connection, {
sandbox,
example,
async alert(content) {
console.log(
`Alert API used by example ${example} in the worker sandbox`,
);
window.alert(content);
},
await sandboxToUse.imports.render(receiver.connection, {
...api,
});
}
},
Expand Down
1 change: 1 addition & 0 deletions examples/kitchen-sink/app/host/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export function ControlPanel({
<option value="svelte">Svelte</option>
<option value="vue">Vue</option>
<option value="htm">htm</option>
<option value="react-remote-ui">React Remote UI</option>
</Select>
</section>

Expand Down
96 changes: 96 additions & 0 deletions examples/kitchen-sink/app/remote/examples/react-remote-ui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/** @jsxRuntime automatic */
/** @jsxImportSource react */
import {retain} from '@quilted/threads';

import {createRemoteReactComponent} from '@remote-ui/react';
import {
ButtonProperties,
ModalProperties,
RenderAPI,
StackProperties,
TextProperties,
} from '../../types';
import {useState} from 'react';
import {createRoot, createRemoteRoot} from '@remote-ui/react';
import {RemoteChannel} from '@remote-ui/core';

const Button = createRemoteReactComponent<
'Button',
ButtonProperties & {modal?: React.ReactNode}
>('Button', {fragmentProps: ['modal']});

const Text = createRemoteReactComponent<'Text', TextProperties>('Text');
const Stack = createRemoteReactComponent<'Stack', StackProperties>('Stack');
const Modal = createRemoteReactComponent<
'Modal',
ModalProperties & {primaryAction?: React.ReactNode}
>('Modal', {fragmentProps: ['primaryAction']});

export function renderUsingReactRemoteUI(
channel: RemoteChannel,
api: RenderAPI,
) {
retain(api);
retain(channel);

const remoteRoot = createRemoteRoot(channel, {
components: ['Button', 'Text', 'Stack', 'Modal'],
});

createRoot(remoteRoot).render(<App api={api} />);
remoteRoot.mount();
}

function App({api}: {api: RenderAPI}) {
return (
<Stack spacing>
<Text>
Rendering example: <Text emphasis>{api.example}</Text>
</Text>
<Text>
Rendering in sandbox: <Text emphasis>{api.sandbox}</Text>
</Text>
<Button modal={<CountModal {...api} />}>Open modal</Button>
</Stack>
);
}

function CountModal({alert, closeModal}: RenderAPI) {
const [count, setCount] = useState(0);

const primaryAction = (
<Button
onPress={() => {
closeModal();
}}
>
Close
</Button>
);

return (
<Modal
primaryAction={primaryAction}
onClose={() => {
if (count > 0) {
alert(`You clicked ${count} times!`);
}

setCount(0);
}}
>
<Stack spacing>
<Text>
Click count: <Text emphasis>{count}</Text>
</Text>
<Button
onPress={() => {
setCount((count) => count + 1);
}}
>
Click me!
</Button>
</Stack>
</Modal>
);
}
1 change: 1 addition & 0 deletions examples/kitchen-sink/app/remote/examples/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const Button = createRemoteComponent('ui-button', ButtonElement, {
onPress: {event: 'press'},
},
});

const Stack = createRemoteComponent('ui-stack', StackElement);
const Modal = createRemoteComponent('ui-modal', ModalElement, {
eventProps: {
Expand Down
5 changes: 4 additions & 1 deletion examples/kitchen-sink/app/remote/iframe/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {RemoteMutationObserver} from '@remote-dom/core/elements';
import {ThreadNestedIframe} from '@quilted/threads';

import '../elements.ts';
import {render} from '../render.ts';
import {render, renderRemoteUi} from '../render.ts';
import type {SandboxAPI} from '../../types.ts';

// We use the `@quilted/threads` library to create a “thread” for our iframe,
Expand Down Expand Up @@ -32,5 +32,8 @@ new ThreadNestedIframe<never, SandboxAPI>({

await render(root, api);
},
async renderRemoteUi(channel, api) {
await renderRemoteUi(channel, api);
},
},
});
8 changes: 8 additions & 0 deletions examples/kitchen-sink/app/remote/render.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {RemoteChannel} from '@remote-ui/core';
import type {RenderAPI} from '../types.ts';

// Defines the custom elements available to render in the remote environment.
Expand Down Expand Up @@ -31,3 +32,10 @@ export async function render(root: Element, api: RenderAPI) {
}
}
}

export async function renderRemoteUi(channel: RemoteChannel, api: RenderAPI) {
const {renderUsingReactRemoteUI} = await import(
'./examples/react-remote-ui.tsx'
);
return renderUsingReactRemoteUI(channel, api);
}
5 changes: 4 additions & 1 deletion examples/kitchen-sink/app/remote/worker/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import '@remote-dom/react/polyfill';
import {ThreadWebWorker} from '@quilted/threads';

import '../elements.ts';
import {render} from '../render.ts';
import {render, renderRemoteUi} from '../render.ts';
import type {SandboxAPI} from '../../types.ts';

// We use the `@quilted/threads` library to create a “thread” for our iframe,
Expand All @@ -27,5 +27,8 @@ new ThreadWebWorker<never, SandboxAPI>(self as any as Worker, {

await render(root, api);
},
async renderRemoteUi(channel, api) {
await renderRemoteUi(channel, api);
},
},
});
10 changes: 9 additions & 1 deletion examples/kitchen-sink/app/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {RemoteConnection} from '@remote-dom/core';
import {RemoteChannel} from '@remote-ui/core';

/**
* Describes the technology used to sandbox the “remote” code, so that it does
Expand All @@ -25,7 +26,8 @@ export type RenderExample =
| 'preact'
| 'react'
| 'svelte'
| 'vue';
| 'vue'
| 'react-remote-ui';

/**
* The object that the “host” page will pass to the “remote” environment. This
Expand All @@ -49,13 +51,19 @@ export interface RenderAPI {
* alert.
*/
alert(content: string): Promise<void>;

/**
* Closes the modal.
*/
closeModal(): void;
}

/**
*
*/
export interface SandboxAPI {
render(connection: RemoteConnection, api: RenderAPI): Promise<unknown>;
renderRemoteUi(channel: RemoteChannel, api: RenderAPI): Promise<unknown>;
}

// These property and method types will be used by both the host and remote environments.
Expand Down
2 changes: 2 additions & 0 deletions examples/kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"@remote-dom/preact": "workspace:*",
"@remote-dom/react": "workspace:*",
"@remote-dom/signals": "workspace:*",
"@remote-ui/core": "^2.2.4",
"@remote-ui/react": "^5.0.4",
"preact": "^10.22.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.0",
"@playwright/test": "^1.48.2",
"@playwright/test": "^1.49.0",
"@quilted/rollup": "^0.2.45",
"@quilted/typescript": "^0.4.2",
"@quilted/vite": "^0.1.27",
"@types/node": "~20.11.0",
"jsdom": "^25.0.0",
"playwright": "^1.48.2",
"playwright": "^1.49.0",
"prettier": "^3.3.3",
"rollup": "^4.21.0",
"tsx": "^4.19.0",
Expand Down
39 changes: 38 additions & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ import {RemoteFragmentElement} from '@remote-dom/core/elements';
customElements.define('remote-fragment', RemoteFragmentElement);
```

### `@remote-dom/core/receiver`
### `@remote-dom/core/receivers`

A “remote receiver” collects updates that happened in a remote environment, and reconstructs them in a way that allows them to be rendered in the host environment.

Expand Down Expand Up @@ -751,3 +751,40 @@ This helper uses the following logic to determine whether a given property in th
- If the property is an HTML element, it will be appended as a child in a slot named the same as the property (e.g., `<ui-button modal=${html`<ui-modal />`}>` becomes a `ui-modal` child with a `slot="modal"` attribute).
- If the property starts with `on`, the value will be set as an event listener, with the event name being the lowercased version of the string following `on` (e.g., `onClick` sets a `click` event).
- Otherwise, the property will be set as an attribute.

### `@remote-dom/core/legacy`

The `@remote-dom/core/legacy` package provides helpers for adapting between Remote DOM and [`remote-ui`, the previous version of this project](https://github.com/Shopify/remote-dom/discussions/267). These utilities are offered to help you transition to Remote DOM, while continuing to support existing code that expects `remote-ui`-style APIs.

#### Progressive migration from `remote-ui`’s `RemoteChannel` to Remote DOM’s `RemoteConnection`

The `RemoteChannel` and `RemoteConnection` types from `remote-ui` and Remote DOM serve the same purpose: they describe the minimal interface that a remote environment needs to communicate with a host. In Remote DOM, the `RemoteConnection` type has been enhanced in backwards-incompatible ways, in order to support [method calling](#remote-methods), batched updates, and more.

In `remote-ui`, you typically get a `RemoteChannel` function by accessing the `receiver` property on a `RemoteReceiver`, like this:

```ts
import {createRemoteReceiver} from '@remote-ui/core';

const receiver = createRemoteReceiver();
const channel = receiver.receive;

// Do something with the channel, typically by sending it to a remote environment:
sendChannelToRemoteEnvironment(channel);
```

You can migrate to use a Remote DOM [`RemoteReceiver`](#remotereceiver), [`DOMRemoteReceiver`](#domremotereceiver), or [`SignalRemoteReceiver`](/packages/signals/README.md#signalremotereceiver) class, while still supporting the `RemoteChannel` API, by using the `adaptToLegacyRemoteChannel()` function:

You can adapt a `RemoteConnection` to a `RemoteChannel` using this library’s `adaptToLegacyRemoteChannel()` function. This function takes a `RemoteConnection` and returns a `RemoteChannel`, which allows you to use a Remote DOM receiver class on the host, even if the remote environment is using `remote-ui`. This same technique works regardless of whether you are using the [`RemoteReceiver`](#remotereceiver), [`DOMRemoteReceiver`](#domremotereceiver), or [`SignalRemoteReceiver`](/packages/signals/README.md#signalremotereceiver) class.

```ts
import {DOMRemoteReceiver} from '@remote-dom/core/receivers';
import {adaptToLegacyRemoteChannel} from '@remote-dom/core/legacy';

const receiver = new DOMRemoteReceiver();
const channel = adaptToLegacyRemoteChannel(receiver.connection);

// Same as before: do something with the channel
sendChannelToRemoteEnvironment(channel);
```

If you use `remote-ui`’s React bindings to render your UI on the host, you will also need to update that code to make use of the new Remote DOM versions of those bindings (available for [Preact](/packages/preact/README.md#host) and [React](/packages/react/README.md#host)). With this change made, though, you can now seamlessly support code written with `remote-ui` or Remote DOM, by using the more powerful Remote DOM receiver classes on the host and adapting them for legacy code.
Loading