Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][POC] remote-dom #2476

Draft
wants to merge 1 commit into
base: unstable
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/shiny-owls-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/ui-extensions-react': minor
'@shopify/ui-extensions': minor
---

wip
3 changes: 3 additions & 0 deletions packages/ui-extensions-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
"dependencies": {
"@remote-ui/async-subscription": "^2.1.12",
"@remote-ui/react": "^5.0.2",
"@remote-dom/react": "^1.2.1",
"react-dom": "^18.3.1",
"@types/react": ">=18.2.67"
},
"peerDependencies": {
Expand All @@ -81,6 +83,7 @@
"@faker-js/faker": "^8.4.1",
"@quilted/react-testing": "^0.5.31",
"@shopify/ui-extensions": "0.0.0-unstable",
"@types/react-dom": "^18.3.0",
"react": "^18.0.0",
"react-reconciler": "0.29.0",
"react-test-renderer": "^18.2.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {Button as BaseButton} from '@shopify/ui-extensions/checkout';
import {createRemoteReactComponent} from '@remote-ui/react';
import type {ReactPropsFromRemoteComponentType} from '@remote-ui/react';
import {createRemoteComponent} from '@remote-dom/react';

// @ts-ignore
export type ButtonProps = ReactPropsFromRemoteComponentType<typeof BaseButton>;

export const Button = createRemoteReactComponent(BaseButton, {
fragmentProps: ['overlay'],
export const Button = createRemoteComponent('ui-button', BaseButton, {
eventProps: {
onPress: {event: 'press'},
},
slots: ['overlay'],
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {Link as BaseLink} from '@shopify/ui-extensions/checkout';
import {createRemoteReactComponent} from '@remote-ui/react';
import type {ReactPropsFromRemoteComponentType} from '@remote-ui/react';
import {createRemoteComponent} from '@remote-dom/react';

export type LinkProps = ReactPropsFromRemoteComponentType<typeof BaseLink>;

export const Link = createRemoteReactComponent(BaseLink, {
fragmentProps: ['overlay'],
});
export const Link = createRemoteComponent('ui-link', BaseLink);
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {Modal as BaseModal} from '@shopify/ui-extensions/checkout';
import {createRemoteReactComponent} from '@remote-ui/react';
import type {ReactPropsFromRemoteComponentType} from '@remote-ui/react';
import {createRemoteComponent} from '@remote-dom/react';

// @ts-ignore
export type ModalProps = ReactPropsFromRemoteComponentType<typeof BaseModal>;

export const Modal = createRemoteReactComponent(BaseModal, {
fragmentProps: ['primaryAction', 'secondaryActions'],
});
export const Modal = createRemoteComponent('ui-modal', BaseModal);
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {View as BaseView} from '@shopify/ui-extensions/checkout';
import {createRemoteReactComponent} from '@remote-ui/react';
//@ts-ignore
import {createRemoteComponent} from '@remote-dom/react';
import type {ReactPropsFromRemoteComponentType} from '@remote-ui/react';

export type ViewProps = ReactPropsFromRemoteComponentType<typeof BaseView>;

export const View = createRemoteReactComponent(BaseView);
export const View = createRemoteComponent('ui-view', BaseView);
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from './PolicyModal';
export * from './ResourceItem';
export * from './ImageGroup';
export * from './shared-checkout-components';

export default {};
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ export {
Tooltip,
type TooltipProps,
View,
type ViewProps,
ToggleButton,
type ToggleButtonProps,
ToggleButtonGroup,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '@remote-dom/react/polyfill';
import type {ReactElement, PropsWithChildren} from 'react';
import {Component} from 'react';
import {render as remoteRender} from '@remote-ui/react';
import {extension} from '@shopify/ui-extensions/customer-account';
import type {
ExtensionTargets,
Expand All @@ -10,6 +10,8 @@ import type {

import {ExtensionApiContext} from './context';

import {createRoot} from 'react-dom/client';

/**
* Registers your React-based UI Extension to run for the selected extension target.
* Additionally, this function will automatically provide the extension API as React
Expand All @@ -26,6 +28,7 @@ import {ExtensionApiContext} from './context';
* which allows you to perform initial asynchronous work like fetching data from your
* own backend.
*/

export function reactExtension<Target extends RenderExtensionTarget>(
target: Target,
render: (
Expand All @@ -41,24 +44,12 @@ export function reactExtension<Target extends RenderExtensionTarget>(
async (root, api) => {
const element = await render(api as ApiForRenderExtension<Target>);

await new Promise<void>((resolve, reject) => {
try {
remoteRender(
<ExtensionApiContext.Provider value={api}>
<ErrorBoundary>{element}</ErrorBoundary>
</ExtensionApiContext.Provider>,
root,
() => {
resolve();
},
);
} catch (error) {
// Workaround for https://github.com/Shopify/ui-extensions/issues/325
// eslint-disable-next-line no-console
console.error(error);
reject(error);
}
});
createRoot(root).render(
<ExtensionApiContext.Provider value={api}>
<ErrorBoundary>{element}</ErrorBoundary>
</ExtensionApiContext.Provider>,
);
return;
},
) as any;
}
Expand All @@ -85,6 +76,7 @@ export function render<Target extends RenderExtensionTarget>(
target: Target,
render: (api: ApiForRenderExtension<Target>) => ReactElement<any>,
): ExtensionTargets[Target] {
//@ts-ignore
return reactExtension(target, render);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/ui-extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"sideEffects": false,
"dependencies": {
"@remote-ui/async-subscription": "^2.1.12",
"@remote-ui/core": "^2.2.4"
"@remote-ui/core": "^2.2.4",
"@remote-dom/core": "^1.5.1"
},
"devDependencies": {
"@shopify/generate-docs": "0.16.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {createRemoteComponent} from '@remote-ui/core';
import {createRemoteElement} from '@remote-dom/core/elements';

import type {
Appearance,
Expand Down Expand Up @@ -85,7 +85,11 @@ export interface ButtonProps
onPress?(): void;
}

/**
* Buttons are used for actions, such as “Add”, “Continue”, “Pay now”, or “Save”.
*/
export const Button = createRemoteComponent<'Button', ButtonProps>('Button');
export const Button = createRemoteElement<{}>({
properties: {to: String, foo: String},
events: {press: {bubbles: true}},
slots: ['overlay'],
});

// @ts-ignore
customElements.define('ui-button', Button);
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {createRemoteComponent} from '@remote-ui/core';

import type {
Appearance,
OverlayActivatorProps,
DisclosureActivatorProps,
} from '../shared';
import {createRemoteElement} from '@remote-dom/core/elements';

export interface LinkProps
extends OverlayActivatorProps,
Expand Down Expand Up @@ -48,7 +47,9 @@ export interface LinkProps
onPress?(): void;
}

/**
* Link makes text interactive so customers can perform an action, such as navigating to another location.
*/
export const Link = createRemoteComponent<'Link', LinkProps>('Link');
export const Link = createRemoteElement<{}>({
properties: {to: String},
});

// @ts-ignore
customElements.define('ui-link', Link);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {createRemoteElement} from '@remote-dom/core/elements';
import type {RemoteFragment} from '@remote-ui/core';
import {createRemoteComponent} from '@remote-ui/core';

export interface ModalProps {
/**
Expand Down Expand Up @@ -57,4 +57,11 @@ export interface ModalProps {
*
* The library automatically applies the [WAI-ARIA Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) to both the activator and the modal content.
*/
export const Modal = createRemoteComponent<'Modal', ModalProps>('Modal');
// export const Modal = createRemoteComponent<'Modal', ModalProps>('Modal');

export const Modal = createRemoteElement<{}>({
properties: {id: String},
});

// @ts-ignore
customElements.define('ui-modal', Modal);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {createRemoteComponent} from '@remote-ui/core';
import {createRemoteElement} from '@remote-dom/core/elements';

import type {MaybeResponsiveConditionalStyle} from '../../style/types';
import type {
Expand Down Expand Up @@ -234,4 +234,9 @@ export interface ViewProps
* “natural” size, so this component can be useful in layout components (like `Grid`,
* `BlockStack`, `InlineStack`) that would otherwise stretch their children to fit.
*/
export const View = createRemoteComponent<'View', ViewProps>('View');
export const View = createRemoteElement<{}>({
properties: {padding: String, border: String},
});

// @ts-ignore
customElements.define('ui-view', View);
41 changes: 29 additions & 12 deletions packages/ui-extensions/src/utilities/registration.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import {createRemoteRoot} from '@remote-ui/core';
import '@remote-dom/core/polyfill';

import type {
RenderExtensionConnection,
RenderExtension,
RenderExtensionWithRemoteRoot,
} from '../extension';

import {
// @ts-ignore
BatchingRemoteConnection,
RemoteFragmentElement,
// @ts-ignore
RemoteMutationObserver,
RemoteRootElement,
} from '@remote-dom/core/elements';

console.log('### RemoteRootElement', RemoteRootElement);
// @ts-ignore
customElements.define('remote-root', RemoteRootElement);
// @ts-ignore
customElements.define('remote-fragment', RemoteFragmentElement);

export interface ExtensionRegistrationFunction<ExtensionTargets> {
<Target extends keyof ExtensionTargets>(
target: Target,
Expand Down Expand Up @@ -46,35 +61,37 @@ export function createExtensionRegistrationFunction<
return (implementation as any)(...args);
}

const [{channel, components}, api] = args as [
RenderExtensionConnection,
any,
];
const [{channel}, api] = args as [RenderExtensionConnection, any];

// @ts-ignore
const root = document.createElement('remote-root');
root.connect(channel);

const root = createRemoteRoot(channel, {
components,
strict: true,
});
// try {
// const observer = new RemoteMutationObserver(channel);
// observer.observe(root);
// } catch {
// }

let renderResult = (implementation as any)(root, api);

console.log('### renderResult test', renderResult);

if (
typeof renderResult === 'object' &&
renderResult != null &&
'then' in renderResult
) {
renderResult = await renderResult;
console.log('### renderResult promise', renderResult);
}

root.mount();

return renderResult;
}

(globalThis as any).shopify?.extend(target, extension);

return extension as any;
};

return extensionWrapper;
}
Loading
Loading