
Shopify Polaris Checkout Extensions
- 6.6k installs
- 476 repo stars
- Updated July 27, 2026
- shopify/shopify-ai-toolkit
shopify-polaris-checkout-extensions agent skill from cached repository docs.
About
admin name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 storefront-graphql name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 partner name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 customer name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 payments-apps name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 polaris-app-home polaris-admin-extensions name 2026-04 releaseCandidate true name 2026-01 latestVersion true name 2025-10 name 2025-07 polaris-checkout-extensions name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 polaris-customer-account-extensions name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 pos-ui name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 hydrogen name 2026-.
- See SKILL.md documentation
- See SKILL.md documentation
- See SKILL.md documentation
- See SKILL.md documentation
- See SKILL.md documentation
Shopify Polaris Checkout Extensions by the numbers
- 6,594 all-time installs (skills.sh)
- +280 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #234 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
shopify-polaris-checkout-extensions capabilities & compatibility
- Capabilities
- see skill.md documentation
- Use cases
- documentation
What shopify-polaris-checkout-extensions says it does
{ "admin": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "
npx skills add https://github.com/shopify/shopify-ai-toolkit --skill shopify-polaris-checkout-extensionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.6k |
|---|---|
| repo stars | ★ 476 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | shopify/shopify-ai-toolkit ↗ |
When should developers use shopify-polaris-checkout-extensions and what problem does it solve?
admin name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 storefront-graphql name unstable name 2026-07 releaseCandidate true name 2
Who is it for?
Developers working with shopify-polaris-checkout-extensions patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
User asks about shopify-polaris-checkout-extensions.
What you get
Grounded guidance and workflows from SKILL.md for shopify-polaris-checkout-extensions.
- checkout UI extension code
- API version compatibility config
By the numbers
- Tracks 3 API families: admin, storefront-graphql, and partner
- Lists 2026-04 as latestVersion and 2026-07 as releaseCandidate
Files
{ "admin": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "storefront-graphql": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "partner": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "customer": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "payments-apps": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "polaris-app-home": [], "polaris-admin-extensions": [ { "name": "2026-04", "releaseCandidate": true }, { "name": "2026-01", "latestVersion": true }, { "name": "2025-10" }, { "name": "2025-07" } ], "polaris-checkout-extensions": [ { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "polaris-customer-account-extensions": [ { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "pos-ui": [ { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "hydrogen": [ { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "storefront-web-components": [], "functions_cart_checkout_validation": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "functions_cart_transform": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "functions_delivery_customization": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, { "name": "2026-04", "latestVersion": true }, { "name": "2026-01" }, { "name": "2025-10" }, { "name": "2025-07" } ], "functions_discount": [ { "name": "unstable" }, { "name": "2026-07", "releaseCandidate": true }, {
{
"admin": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"storefront-graphql": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"partner": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"customer": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"payments-apps": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"polaris-app-home": [],
"polaris-admin-extensions": [
{
"name": "2026-04",
"releaseCandidate": true
},
{
"name": "2026-01",
"latestVersion": true
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"polaris-checkout-extensions": [
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"polaris-customer-account-extensions": [
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"pos-ui": [
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"hydrogen": [
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"storefront-web-components": [],
"functions_cart_checkout_validation": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_cart_transform": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_delivery_customization": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_discount": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_discounts_allocator": [
{
"name": "unstable",
"latestVersion": true
}
],
"functions_fulfillment_constraints": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_local_pickup_delivery_option_generator": [
{
"name": "unstable",
"latestVersion": true
}
],
"functions_order_discounts": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_order_routing_location_rule": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_payment_customization": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_pickup_point_delivery_option_generator": [
{
"name": "unstable",
"latestVersion": true
}
],
"functions_product_discounts": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
],
"functions_shipping_discounts": [
{
"name": "unstable"
},
{
"name": "2026-07",
"releaseCandidate": true
},
{
"name": "2026-04",
"latestVersion": true
},
{
"name": "2026-01"
},
{
"name": "2025-10"
},
{
"name": "2025-07"
}
]
}
import {createPackage, Runtime} from '@shopify/loom';
import {defaultProjectPlugin} from '../../config/loom';
export default createPackage((pkg) => {
pkg.runtimes(Runtime.Node, Runtime.Browser);
pkg.entry({root: './src/index.ts'});
pkg.use(defaultProjectPlugin());
});
{
"name": "@remote-ui/async-subscription",
"version": "2.1.18",
"publishConfig": {
"access": "public",
"@remote-ui:registry": "https://registry.npmjs.org"
},
"sideEffects": false,
"main": "index.js",
"module": "index.mjs",
"esnext": "index.esnext",
"types": "./build/ts/index.d.ts",
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
}
},
"license": "MIT",
"dependencies": {
"@remote-ui/rpc": "^1.4.7"
}
}
import {retain, release} from '@remote-ui/rpc';
import type {SyncSubscribable, RemoteSubscribable} from './types';
export function createRemoteSubscribable<T>(
subscription: SyncSubscribable<T>,
): RemoteSubscribable<T> {
const initial = subscription.current;
return {
initial,
subscribe(subscriber) {
retain(subscriber);
const unsubscribe = subscription.subscribe(
(value = subscription.current) => {
return subscriber(value);
},
);
const teardown = () => {
unsubscribe();
release(subscriber);
};
return [teardown, subscription.current];
},
};
}
export {makeStatefulSubscribable} from './stateful';
export {createRemoteSubscribable} from './create';
export type {
SyncSubscribable,
RemoteSubscribable,
StatefulRemoteSubscribable,
} from './types';
import {retain, release} from '@remote-ui/rpc';
import type {
Subscriber,
RemoteSubscribable,
RemoteSubscribeResult,
StatefulRemoteSubscribable,
} from './types';
export function makeStatefulSubscribable<T>(
subscription: RemoteSubscribable<T>,
): StatefulRemoteSubscribable<T> {
// We retain because it will automatically retain any functions we get from
// calling functions on this object, which will automatically manage the memory
// for unsubscribe callbacks received from subscription.subscribe().
retain(subscription);
let current = subscription.initial;
let listening = true;
let hasUpdated = false;
const subscribers = new Set<Subscriber<T>>();
const subscriptionResult = Promise.resolve<RemoteSubscribeResult<T>>(
subscription.subscribe(listener),
).then((result) => {
// Because of the async nature of receiving the result, we may have
// already started receiving updates from the subscriber before this
// code has been reached. In that case, we do not want to apply the
// value we received on subscribing, because it is already out of date.
if (!hasUpdated) listener(result[1]);
return result;
});
return {
get current() {
return current;
},
subscribe(subscriber) {
subscribers.add(subscriber);
return () => {
subscribers.delete(subscriber);
};
},
async destroy() {
listening = false;
subscribers.clear();
const [unsubscribe] = await subscriptionResult;
unsubscribe();
release(subscription);
},
};
function listener(value: T) {
hasUpdated = true;
if (!listening || current === value) return;
current = value;
for (const subscriber of subscribers) {
subscriber(current);
}
}
}
import type {MaybePromise} from '@remote-ui/rpc';
export type Subscriber<T> = (value: T) => unknown;
export type RemoteSubscribeResult<T> = [() => void, T];
export interface SyncSubscribable<T> {
readonly current: T;
subscribe(subscriber: Subscriber<T>): () => void;
}
export interface RemoteSubscribable<T> {
readonly initial: T;
subscribe(subscriber: Subscriber<T>): MaybePromise<RemoteSubscribeResult<T>>;
}
export interface StatefulRemoteSubscribable<T> extends SyncSubscribable<T> {
destroy(): Promise<void>;
}
import {createPackage, Runtime} from '@shopify/loom';
import {defaultProjectPlugin} from '../../config/loom';
export default createPackage((pkg) => {
pkg.runtimes(Runtime.Node, Runtime.Browser);
pkg.entry({root: './src/index.ts'});
pkg.use(defaultProjectPlugin());
});
{
"name": "@remote-ui/core",
"version": "2.2.7",
"publishConfig": {
"access": "public",
"@remote-ui:registry": "https://registry.npmjs.org"
},
"license": "MIT",
"main": "index.js",
"module": "index.mjs",
"esnext": "index.esnext",
"types": "./build/ts/index.d.ts",
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
}
},
"sideEffects": false,
"dependencies": {
"@remote-ui/rpc": "^1.4.7",
"@remote-ui/types": "^1.1.3"
}
}
import {RemoteComponentType} from '@remote-ui/types';
export function createRemoteComponent<
Type extends string,
Props = Record<string, never>,
AllowedChildren extends RemoteComponentType<string, any> | boolean = true,
>(
componentType: Type,
): Type & RemoteComponentType<Type, Props, AllowedChildren> {
return componentType as any;
}
export type {
RemoteComponentType,
PropsForRemoteComponent,
AllowedChildrenForRemoteComponent,
IdentifierForRemoteComponent,
} from '@remote-ui/types';
export {retain, release} from '@remote-ui/rpc';
export {createRemoteComponent} from './component';
export {
ACTION_MOUNT,
ACTION_INSERT_CHILD,
ACTION_REMOVE_CHILD,
ACTION_UPDATE_PROPS,
ACTION_UPDATE_TEXT,
KIND_ROOT,
KIND_COMPONENT,
KIND_TEXT,
KIND_FRAGMENT,
} from './types';
export type {
RemoteRoot,
RemoteChannel,
RemoteComponent,
RemoteText,
RemoteChild,
RemoteFragment,
Serialized,
ActionArgumentMap,
RemoteTextSerialization,
RemoteComponentSerialization,
RemoteFragmentSerialization,
} from './types';
export {createRemoteRoot} from './root';
export {
createRemoteReceiver,
createRemoteChannel,
isRemoteFragmentSerialization,
isRemoteReceiverAttachableFragment,
} from './receiver';
export type {
RemoteReceiver,
RemoteReceiverAttachment,
RemoteReceiverAttachable,
RemoteReceiverAttachableChild,
RemoteReceiverAttachableRoot,
RemoteReceiverAttachableComponent,
RemoteReceiverAttachableText,
RemoteReceiverAttachableFragment,
} from './receiver';
export {isRemoteComponent, isRemoteText, isRemoteFragment} from './utilities';
import {retain, release} from '@remote-ui/rpc';
import {
ACTION_MOUNT,
ACTION_INSERT_CHILD,
ACTION_REMOVE_CHILD,
ACTION_UPDATE_PROPS,
ACTION_UPDATE_TEXT,
KIND_COMPONENT,
KIND_FRAGMENT,
KIND_ROOT,
} from './types';
import type {
ActionArgumentMap,
RemoteChannel,
RemoteTextSerialization,
RemoteComponentSerialization,
RemoteFragmentSerialization,
} from './types';
import {isRemoteFragment} from './utilities';
export const ROOT_ID = Symbol('RootId');
export interface RemoteReceiverAttachableText extends RemoteTextSerialization {
version: number;
}
export interface RemoteReceiverAttachableComponent
extends Omit<RemoteComponentSerialization<any>, 'children'> {
children: RemoteReceiverAttachableChild[];
version: number;
}
export interface RemoteReceiverAttachableFragment
extends Omit<RemoteFragmentSerialization, 'children'> {
children: RemoteReceiverAttachableChild[];
version: number;
}
export interface RemoteReceiverAttachableRoot {
id: typeof ROOT_ID;
kind: typeof KIND_ROOT;
children: RemoteReceiverAttachableChild[];
version: number;
}
export type RemoteReceiverAttachableChild =
| RemoteReceiverAttachableText
| RemoteReceiverAttachableComponent;
export type RemoteReceiverAttachable =
| RemoteReceiverAttachableChild
| RemoteReceiverAttachableRoot
| RemoteReceiverAttachableFragment;
interface RemoteChannelRunner {
mount(...args: ActionArgumentMap[typeof ACTION_MOUNT]): void;
insertChild(...args: ActionArgumentMap[typeof ACTION_INSERT_CHILD]): void;
removeChild(...args: ActionArgumentMap[typeof ACTION_INSERT_CHILD]): void;
updateProps(...args: ActionArgumentMap[typeof ACTION_UPDATE_PROPS]): void;
updateText(...args: ActionArgumentMap[typeof ACTION_UPDATE_TEXT]): void;
}
export function createRemoteChannel({
mount,
insertChild,
removeChild,
updateProps,
updateText,
}: RemoteChannelRunner): RemoteChannel {
const messageMap = new Map<keyof ActionArgumentMap, (...args: any[]) => any>([
[ACTION_MOUNT, mount],
[ACTION_REMOVE_CHILD, removeChild],
[ACTION_INSERT_CHILD, insertChild],
[ACTION_UPDATE_PROPS, updateProps],
[ACTION_UPDATE_TEXT, updateText],
]);
return (type, ...args) => messageMap.get(type)!(...args);
}
export interface RemoteReceiverAttachment {
readonly root: RemoteReceiverAttachableRoot;
get<T extends RemoteReceiverAttachable>(attachable: Pick<T, 'id'>): T | null;
subscribe<T extends RemoteReceiverAttachable>(
{id}: T,
subscriber: (value: T) => void,
): () => void;
}
export interface RemoteReceiver {
readonly receive: RemoteChannel;
readonly attached: RemoteReceiverAttachment;
readonly state: 'mounted' | 'unmounted';
on(event: 'mount', handler: () => void): () => void;
flush(): Promise<void>;
}
export function createRemoteReceiver(): RemoteReceiver {
const queuedUpdates = new Set<RemoteReceiverAttachable>();
const listeners = new Map<
Parameters<RemoteReceiver['on']>[0],
Set<Parameters<RemoteReceiver['on']>[1]>
>();
const attachmentSubscribers = new Map<
string | typeof ROOT_ID,
Set<(value: RemoteReceiverAttachable) => void>
>();
let timeout: Promise<void> | null = null;
let state: RemoteReceiver['state'] = 'unmounted';
const root: RemoteReceiverAttachableRoot = {
id: ROOT_ID,
kind: KIND_ROOT,
children: [],
version: 0,
};
const attachedNodes = new Map<
string | typeof ROOT_ID,
RemoteReceiverAttachable
>([[ROOT_ID, root]]);
const receive = createRemoteChannel({
mount: (children) => {
const root = attachedNodes.get(ROOT_ID) as RemoteReceiverAttachableRoot;
const normalizedChildren = children.map((child) =>
normalizeNode(child, addVersion),
);
root.version += 1;
root.children = normalizedChildren;
state = 'mounted';
for (const child of normalizedChildren) {
retain(child);
attach(child);
}
// eslint-disable-next-line promise/catch-or-return
enqueueUpdate(root).then(() => {
emit('mount');
});
},
insertChild: (id, index, child, existingId) => {
const attached = attachedNodes.get(
id ?? ROOT_ID,
) as RemoteReceiverAttachableRoot;
const {children} = attached;
let existingAttached: RemoteReceiverAttachableRoot | undefined;
let normalizedChild: RemoteReceiverAttachableChild;
if (id === existingId) {
existingAttached = attached;
} else if (existingId !== false) {
existingAttached = attachedNodes.get(
existingId ?? ROOT_ID,
) as RemoteReceiverAttachableRoot;
}
if (existingAttached) {
const childId = child.id;
const existingChildren = existingAttached.children;
const existingIndex = existingChildren.findIndex(
(child) => child.id === childId,
);
const [removed] = existingChildren.splice(existingIndex, 1);
normalizedChild = removed;
// If we are just moving the child to a different index in the same node,
// we don’t need to enqueue an update, because that will be done for this
// node below.
if (id !== existingId) {
existingAttached.version += 1;
enqueueUpdate(existingAttached);
}
} else {
normalizedChild = normalizeNode(child, addVersion);
retain(normalizedChild);
attach(normalizedChild);
}
if (index === children.length) {
children.push(normalizedChild);
} else {
children.splice(index, 0, normalizedChild);
}
attached.version += 1;
enqueueUpdate(attached);
},
removeChild: (id, index) => {
const attached = attachedNodes.get(
id ?? ROOT_ID,
) as RemoteReceiverAttachableRoot;
const {children} = attached;
const [removed] = children.splice(index, 1);
if (!removed) {
return;
}
attached.version += 1;
detach(removed);
// eslint-disable-next-line promise/catch-or-return
enqueueUpdate(attached).then(() => {
release(removed);
});
},
updateProps: (id, newProps) => {
const component = attachedNodes.get(
id,
) as RemoteReceiverAttachableComponent;
const oldProps = {...(component.props as any)};
retain(newProps);
Object.keys(newProps).forEach((key) => {
const newProp = (newProps as any)[key];
const oldProp = (oldProps as any)[key];
if (isRemoteReceiverAttachableFragment(oldProp)) {
detach(oldProp);
}
if (isRemoteFragmentSerialization(newProp)) {
const attachableNewProp = normalizeNode(newProp, addVersion);
attach(attachableNewProp);
}
});
Object.assign(component.props as any, newProps);
component.version += 1;
// eslint-disable-next-line promise/catch-or-return
enqueueUpdate(component).then(() => {
for (const key of Object.keys(newProps)) {
release((oldProps as any)[key]);
}
});
},
updateText: (id, newText) => {
const text = attachedNodes.get(id) as RemoteReceiverAttachableText;
text.text = newText;
text.version += 1;
enqueueUpdate(text);
},
});
return {
get state() {
return state;
},
receive,
attached: {
root,
get({id}) {
return (attachedNodes.get(id) as any) ?? null;
},
subscribe({id}, subscriber) {
let subscribers = attachmentSubscribers.get(id);
if (subscribers == null) {
subscribers = new Set();
attachmentSubscribers.set(id, subscribers);
}
subscribers.add(subscriber as any);
return () => {
const subscribers = attachmentSubscribers.get(id);
if (subscribers) {
subscribers.delete(subscriber as any);
if (subscribers.size === 0) {
attachmentSubscribers.delete(id);
}
}
};
},
},
flush,
on(event, listener) {
let listenersForEvent = listeners.get(event);
if (listenersForEvent == null) {
listenersForEvent = new Set();
listeners.set(event, listenersForEvent);
}
listenersForEvent.add(listener);
return () => {
const listenersForEvent = listeners.get(event);
if (listenersForEvent) {
listenersForEvent.delete(listener);
if (listenersForEvent.size === 0) {
listeners.delete(event);
}
}
};
},
};
function flush() {
return timeout ?? Promise.resolve();
}
function emit(event: 'mount') {
const listenersForEvent = listeners.get(event);
if (listenersForEvent) {
for (const listener of listenersForEvent) {
listener();
}
}
}
function enqueueUpdate(attached: RemoteReceiverAttachable) {
timeout =
timeout ??
new Promise((resolve) => {
setTimeout(() => {
const attachedToUpdate = [...queuedUpdates];
timeout = null;
queuedUpdates.clear();
for (const attached of attachedToUpdate) {
const subscribers = attachmentSubscribers.get(attached.id);
if (subscribers) {
for (const subscriber of subscribers) {
subscriber(attached);
}
}
}
resolve();
}, 0);
});
queuedUpdates.add(attached);
return timeout;
}
function attach(
child: RemoteReceiverAttachableChild | RemoteReceiverAttachableFragment,
) {
attachedNodes.set(child.id, child);
if (child.kind === KIND_COMPONENT && 'props' in child) {
const {props = {}} = child as any;
Object.keys(props).forEach((key) => {
const prop = props[key];
if (!isRemoteReceiverAttachableFragment(prop)) return;
attach(prop);
});
}
if ('children' in child) {
for (const grandChild of child.children) {
attach(grandChild);
}
}
}
function detach(
child: RemoteReceiverAttachableChild | RemoteReceiverAttachableFragment,
) {
attachedNodes.delete(child.id);
if (child.kind === KIND_COMPONENT && 'props' in child) {
const {props = {}} = child as any;
Object.keys(props).forEach((key) => {
const prop = props[key];
if (!isRemoteReceiverAttachableFragment(prop)) return;
detach(prop);
});
}
if ('children' in child) {
for (const grandChild of child.children) {
detach(grandChild);
}
}
}
}
function addVersion<T>(
value: T,
): T extends RemoteTextSerialization
? RemoteReceiverAttachableText
: T extends RemoteComponentSerialization
? RemoteReceiverAttachableChild
: T extends RemoteFragmentSerialization
? RemoteReceiverAttachableFragment
: never {
(value as any).version = 0;
return value as any;
}
function normalizeNode<
T extends
| RemoteTextSerialization
| RemoteComponentSerialization
| RemoteFragmentSerialization,
R,
>(node: T, normalizer: (node: T) => R) {
if (node.kind === KIND_FRAGMENT || node.kind === KIND_COMPONENT) {
(node as any).children.forEach((child: T) =>
normalizeNode(child, normalizer),
);
}
if (node.kind === KIND_COMPONENT && 'props' in node) {
const {props} = node as any;
for (const key of Object.keys(props)) {
const prop = props[key];
if (!isRemoteFragmentSerialization(prop)) continue;
props[key] = normalizeNode(prop as any, normalizer);
}
}
return normalizer(node);
}
export function isRemoteFragmentSerialization(
object: unknown,
): object is RemoteFragmentSerialization {
return isRemoteFragment(object) && 'id' in object && 'children' in object;
}
export function isRemoteReceiverAttachableFragment(
object: unknown,
): object is RemoteReceiverAttachableFragment {
return isRemoteFragmentSerialization(object) && 'version' in object;
}
import type {RemoteComponentType} from '@remote-ui/types';
import {isBasicObject} from '@remote-ui/rpc';
import {
ACTION_MOUNT,
ACTION_INSERT_CHILD,
ACTION_REMOVE_CHILD,
ACTION_UPDATE_PROPS,
ACTION_UPDATE_TEXT,
KIND_ROOT,
KIND_COMPONENT,
KIND_TEXT,
KIND_FRAGMENT,
} from './types';
import type {
Serialized,
RemoteRoot,
RemoteText,
RemoteChannel,
RemoteComponent,
RemoteFragment,
RemoteRootOptions,
RemoteFragmentSerialization,
} from './types';
import {isRemoteFragment} from './utilities';
type AnyChild = RemoteText<any> | RemoteComponent<any, any>;
type AnyNode = AnyChild | RemoteFragment<any>;
type AnyParent =
| RemoteRoot<any, any>
| RemoteComponent<any, any>
| RemoteFragment<any>;
interface RootInternals {
strict: boolean;
mounted: boolean;
channel: RemoteChannel;
nodes: WeakSet<AnyNode>;
tops: WeakMap<AnyNode, AnyParent>;
parents: WeakMap<AnyNode, AnyParent>;
components: WeakMap<RemoteComponent<any, any>, ComponentInternals>;
fragments: WeakMap<RemoteFragment<any>, FragmentInternals>;
children: ReadonlyArray<AnyChild>;
}
interface ComponentInternals {
externalProps: {readonly [key: string]: any};
internalProps: {readonly [key: string]: any};
children: ReadonlyArray<AnyChild>;
}
interface FragmentInternals {
children: ReadonlyArray<AnyChild>;
}
type ParentInternals = RootInternals | ComponentInternals | FragmentInternals;
interface TextInternals {
text: string;
}
const FUNCTION_CURRENT_IMPLEMENTATION_KEY = '__current';
const EMPTY_OBJECT = {} as any;
const EMPTY_ARRAY: any[] = [];
type HotSwappableFunction<T extends (...args: any[]) => any> = T & {
[FUNCTION_CURRENT_IMPLEMENTATION_KEY]: any;
};
export function createRemoteRoot<
AllowedComponents extends RemoteComponentType<
string,
any
> = RemoteComponentType<any, any>,
AllowedChildrenTypes extends AllowedComponents | boolean = true,
>(
channel: RemoteChannel,
{strict = true, components}: RemoteRootOptions<AllowedComponents> = {},
): RemoteRoot<AllowedComponents, AllowedChildrenTypes> {
type Root = RemoteRoot<AllowedComponents, AllowedChildrenTypes>;
let currentId = 0;
const rootInternals: RootInternals = {
strict,
mounted: false,
channel,
children: EMPTY_ARRAY,
nodes: new WeakSet(),
parents: new WeakMap(),
tops: new WeakMap(),
components: new WeakMap(),
fragments: new WeakMap(),
};
if (strict) Object.freeze(components);
const remoteRoot: Root = {
kind: KIND_ROOT,
options: strict
? Object.freeze({strict, components})
: {strict, components},
get children() {
return rootInternals.children as any;
},
createComponent(type, ...rest) {
if (components && components.indexOf(type) < 0) {
throw new Error(`Unsupported component: ${type}`);
}
const [initialProps, initialChildren, ...moreChildren] = rest;
const normalizedInitialProps = initialProps ?? {};
const normalizedInitialChildren: AnyChild[] = [];
const normalizedInternalProps: {[key: string]: any} = {};
if (initialProps) {
for (const key of Object.keys(initialProps)) {
// "children" as a prop can be extremely confusing with the "children" of
// a component. In React, a "child" can be anything, but once it reaches
// a host environment (like this remote `Root`), we want "children" to have
// only one meaning: the actual, resolved children components and text.
//
// To enforce this, we delete any prop named "children". We don’t take a copy
// of the props for performance, so a user calling this function must do so
// with an object that can handle being mutated.
if (key === 'children') continue;
normalizedInternalProps[key] = makeValueHotSwappable(
serializeProp(initialProps[key]),
);
}
}
if (initialChildren) {
if (Array.isArray(initialChildren)) {
for (const child of initialChildren) {
normalizedInitialChildren.push(normalizeChild(child, remoteRoot));
}
} else {
normalizedInitialChildren.push(
normalizeChild(initialChildren, remoteRoot),
);
// The complex tuple type of `rest` makes it so `moreChildren` is
// incorrectly inferred as potentially being the props of the component,
// lazy casting since we know it will be an array of child elements
// (or empty).
for (const child of moreChildren as any[]) {
normalizedInitialChildren.push(normalizeChild(child, remoteRoot));
}
}
}
const id = `${currentId++}`;
const internals: ComponentInternals = {
externalProps: strict
? Object.freeze(normalizedInitialProps)
: normalizedInitialProps,
internalProps: normalizedInternalProps,
children: strict
? Object.freeze(normalizedInitialChildren)
: normalizedInitialChildren,
};
const component: RemoteComponent<AllowedComponents, Root> = {
kind: KIND_COMPONENT,
get children() {
return internals.children;
},
get props() {
return internals.externalProps;
},
get remoteProps() {
return internals.internalProps;
},
remove: () => remove(component),
updateProps: (newProps) =>
updateProps(component, newProps, internals, rootInternals),
append: (...children) =>
append(
component,
children.map((child) => normalizeChild(child, remoteRoot)),
internals,
rootInternals,
),
appendChild: (child) =>
appendChild(
component,
normalizeChild(child, remoteRoot),
internals,
rootInternals,
),
removeChild: (child) =>
removeChild(component, child, internals, rootInternals),
replaceChildren: (...children) =>
replaceChildren(
component,
children.map((child) => normalizeChild(child, remoteRoot)),
internals,
rootInternals,
),
insertBefore: (child, before) =>
insertBefore(
component,
normalizeChild(child, remoteRoot),
before,
internals,
rootInternals,
),
insertChildBefore: (child, before) =>
insertBefore(
component,
normalizeChild(child, remoteRoot),
before,
internals,
rootInternals,
),
// Just satisfying the type definition, since we need to write
// some properties manually, which we do below. If we just `as any`
// the whole object, we lose the implicit argument types for the
// methods above.
...EMPTY_OBJECT,
};
rootInternals.components.set(component, internals);
Object.defineProperty(component, 'type', {
value: type,
configurable: false,
writable: false,
enumerable: true,
});
makePartOfTree(component, rootInternals);
makeRemote(component, id, remoteRoot);
for (const child of internals.children) {
moveNodeToContainer(component, child, rootInternals);
}
return component;
},
createText(content = '') {
const id = `${currentId++}`;
const internals: TextInternals = {text: content};
const update: RemoteText<Root>['update'] = (newText) =>
updateText(text, newText, internals, rootInternals);
const text: RemoteText<Root> = {
kind: KIND_TEXT,
get text() {
return internals.text;
},
update,
updateText: update,
remove: () => remove(text),
// Just satisfying the type definition, since we need to write
// some properties manually.
...EMPTY_OBJECT,
};
makePartOfTree(text, rootInternals);
makeRemote(text, id, remoteRoot);
return text;
},
createFragment() {
const id = `${currentId++}`;
const internals: FragmentInternals = {
children: strict ? Object.freeze([]) : [],
};
const fragment: RemoteFragment<Root> = {
kind: KIND_FRAGMENT,
get children() {
return internals.children;
},
append: (...children) =>
append(
fragment,
children.map((child) => normalizeChild(child, remoteRoot)),
internals,
rootInternals,
),
appendChild: (child) =>
appendChild(
fragment,
normalizeChild(child, remoteRoot),
internals,
rootInternals,
),
removeChild: (child) =>
removeChild(fragment, child, internals, rootInternals),
replaceChildren: (...children) =>
replaceChildren(
fragment,
children.map((child) => normalizeChild(child, remoteRoot)),
internals,
rootInternals,
),
insertBefore: (child, before) =>
insertBefore(
fragment,
normalizeChild(child, remoteRoot),
before,
internals,
rootInternals,
),
insertChildBefore: (child, before) =>
insertBefore(
fragment,
normalizeChild(child, remoteRoot),
before,
internals,
rootInternals,
),
// Just satisfying the type definition, since we need to write
// some properties manually.
...EMPTY_OBJECT,
};
rootInternals.fragments.set(fragment, internals);
makePartOfTree(fragment, rootInternals);
makeRemote(fragment, id, remoteRoot);
return fragment;
},
append: (...children) =>
append(
remoteRoot,
children.map((child) => normalizeChild(child, remoteRoot)),
rootInternals,
rootInternals,
),
appendChild: (child) =>
appendChild(
remoteRoot,
normalizeChild(child, remoteRoot),
rootInternals,
rootInternals,
),
replaceChildren: (...children) =>
replaceChildren(
remoteRoot,
children.map((child) => normalizeChild(child, remoteRoot)),
rootInternals,
rootInternals,
),
removeChild: (child) =>
removeChild(remoteRoot, child, rootInternals, rootInternals),
insertBefore: (child, before) =>
insertBefore(
remoteRoot,
normalizeChild(child, remoteRoot),
before,
rootInternals,
rootInternals,
),
insertChildBefore: (child, before) =>
insertBefore(
remoteRoot,
normalizeChild(child, remoteRoot),
before,
rootInternals,
rootInternals,
),
mount() {
if (rootInternals.mounted) return Promise.resolve();
rootInternals.mounted = true;
return Promise.resolve(
channel(ACTION_MOUNT, rootInternals.children.map(serializeChild)),
);
},
};
return remoteRoot;
}
function connected(element: AnyNode, {tops}: RootInternals) {
return tops.get(element)?.kind === KIND_ROOT;
}
function allDescendants(element: AnyNode, withEach: (item: AnyNode) => void) {
const recurse = (element: AnyNode) => {
if ('children' in element) {
for (const child of element.children) {
withEach(child);
recurse(child);
}
}
};
recurse(element);
}
function perform(
element: AnyChild | AnyParent,
rootInternals: RootInternals,
{
remote,
local,
}: {
remote(channel: RemoteChannel): void | Promise<void>;
local(): void;
},
) {
const {mounted, channel} = rootInternals;
if (
mounted &&
(element.kind === KIND_ROOT || connected(element, rootInternals))
) {
// should only create context once async queue is cleared
remote(channel);
// technically, we should be waiting for the remote update to apply,
// then apply it locally. The implementation below is too naive because
// it allows local updates to get out of sync with remote ones.
// if (remoteResult == null || !('then' in remoteResult)) {
// local();
// return;
// } else {
// return remoteResult.then(() => {
// local();
// });
// }
}
local();
}
function updateText(
text: RemoteText<any>,
newText: string,
internals: TextInternals,
rootInternals: RootInternals,
) {
return perform(text, rootInternals, {
remote: (channel) => channel(ACTION_UPDATE_TEXT, text.id, newText),
local: () => {
internals.text = newText;
},
});
}
type HotSwapRecord = readonly [HotSwappableFunction<any>, any];
const IGNORE = Symbol('ignore');
function updateProps(
component: RemoteComponent<any, any>,
newProps: any,
internals: ComponentInternals,
rootInternals: RootInternals,
) {
const {strict} = rootInternals;
const {internalProps: currentProps, externalProps: currentExternalProps} =
internals;
const normalizedNewProps: {[key: string]: any} = {};
const hotSwapFunctions: HotSwapRecord[] = [];
let hasRemoteChange = false;
for (const key of Object.keys(newProps)) {
// See notes above for why we treat `children` as a reserved prop.
if (key === 'children') continue;
const currentExternalValue = currentExternalProps[key];
const newExternalValue = newProps[key];
const currentValue = currentProps[key];
const newValue = serializeProp(newExternalValue);
// Bail out if we have equal, primitive types
if (
currentValue === newValue &&
(newValue == null || typeof newValue !== 'object')
) {
continue;
}
const [value, hotSwaps] = tryHotSwappingValues(currentValue, newValue);
if (hotSwaps) {
hotSwapFunctions.push(...hotSwaps);
}
if (value === IGNORE) continue;
hasRemoteChange = true;
normalizedNewProps[key] = value;
if (isRemoteFragment(currentExternalValue)) {
removeNodeFromContainer(currentExternalValue, rootInternals);
}
if (isRemoteFragment(newExternalValue)) {
moveNodeToContainer(component, newExternalValue, rootInternals);
}
}
return perform(component, rootInternals, {
remote: (channel) => {
if (hasRemoteChange) {
channel(ACTION_UPDATE_PROPS, component.id, normalizedNewProps);
}
},
local: () => {
const mergedExternalProps = {
...currentExternalProps,
...newProps,
};
internals.externalProps = strict
? Object.freeze(mergedExternalProps)
: mergedExternalProps;
internals.internalProps = {
...internals.internalProps,
...normalizedNewProps,
};
for (const [hotSwappable, newValue] of hotSwapFunctions) {
hotSwappable[FUNCTION_CURRENT_IMPLEMENTATION_KEY] = newValue;
}
},
});
}
// Imagine the following remote-ui components we might render in a remote context:
//
// const root = createRemoteRoot();
// const {value, onChange, onPress} = getPropsForValue();
//
// const textField = root.createComponent('TextField', {value, onChange});
// const button = root.createComponent('Button', {onPress});
//
// root.append(textField);
// root.append(button);
//
// function getPropsForValue(value = '') {
// return {
// value,
// onChange: () => {
// const {value, onChange, onPress} = getPropsForValue();
// textField.updateProps({value, onChange});
// button.updateProps({onPress});
// },
// onPress: () => console.log(value),
// };
// }
//
//
// In this example, assume that the `TextField` `onChange` prop is run on blur.
// If this were running on the host, the following steps would happen if you pressed
// on the button:
//
// 1. The text field blurs, and so calls `onChange()` with its current value, which
// then calls `setValue()` with the updated value.
// 2. We synchronously update the `value`, `onChange`, and `onPress` props to point at
// the most current `value`.
// 3. Handling blur is finished, so the browser now handles the click by calling the
// (newly-updated) `Button` `onPress()`, which logs out the new value.
//
// Because remote-ui reproduces a UI tree asynchronously from the remote context, the
// steps above run in a different order:
//
// 1. The text field blurs, and so calls `onChange()` with its current value.
// 2. Handling blur is finished **from the perspective of the main thread**, so the
// browser now handles the click by calling the (original) `Button` `onPress()`, which
// logs out the **initial** value.
// 3. In the remote context, we receive the `onChange()` call, which calls updates the props
// on the `Button` and `TextField` to be based on the new `value`, but by now it’s
// already too late for `onPress` — the old version has already been called!
//
// As you can see, the timing issue introduced by the asynchronous nature of remote-ui
// can cause “old props” to be called from the main thread. This example may seem like
// an unusual pattern, and it is if you are using `@remote-ui/core` directly; you’d generally
// keep a mutable reference to the state, instead of closing over the state with new props.
// However, abstractions on top of `@remote-ui/core`, like the React reconciler in
// `@remote-ui/react`, work almost entirely by closing over state, so this issue is
// much more common with those declarative libraries.
//
// To protect against this, we handle function props a bit differently. When we have a
// function prop, we replace it with a new function that calls the original. However,
// we make the original mutable, by making it a property on the function itself. When
// this function subsequently updates, we don’t send the update to the main thread (as
// we just saw, this can often be "too late" to be of any use). Instead, we swap out
// the mutable reference to the current implementation of the function prop, which can
// be done synchronously. In the example above, this would all happen synchronously in
// the remote context; in our handling of `TextField onChange()`, we update `Button onPress()`,
// and swap out the implementations. Now, when the main thread attempts to call `Button onPress()`,
// it instead calls our wrapper around the function, which can refer to, and call, the
// most recently-applied implementation, instead of directly calling the old implementation.
type HotSwapResult = [any, HotSwapRecord[]?];
function tryHotSwappingValues(
currentValue: unknown,
newValue: unknown,
seen = new Set<any>(),
): HotSwapResult {
if (seen.has(currentValue)) {
return [IGNORE];
}
if (
typeof currentValue === 'function' &&
FUNCTION_CURRENT_IMPLEMENTATION_KEY in currentValue
) {
seen.add(currentValue);
const result: HotSwapResult = [
typeof newValue === 'function' ? IGNORE : makeValueHotSwappable(newValue),
[[currentValue as HotSwappableFunction<any>, newValue]],
];
return result;
}
if (Array.isArray(currentValue)) {
seen.add(currentValue);
const result = tryHotSwappingArrayValues(currentValue, newValue, seen);
return result;
}
if (isBasicObject(currentValue) && !isRemoteFragment(currentValue)) {
seen.add(currentValue);
const result = tryHotSwappingObjectValues(currentValue, newValue, seen);
return result;
}
const result: HotSwapResult = [currentValue === newValue ? IGNORE : newValue];
return result;
}
function makeValueHotSwappable(
value: unknown,
seen = new Map<any, any>(),
): unknown {
const seenValue = seen.get(value);
if (seenValue) return seenValue;
if (isRemoteFragment(value)) {
seen.set(value, value);
return value;
}
if (Array.isArray(value)) {
const result: any[] = [];
seen.set(value, result);
for (const nested of value) {
result.push(makeValueHotSwappable(nested, seen));
}
return result;
}
if (isBasicObject(value)) {
const result: Record<string, any> = {};
seen.set(value, result);
for (const key of Object.keys(value)) {
result[key] = makeValueHotSwappable((value as any)[key], seen);
}
return result;
}
if (typeof value === 'function') {
const wrappedFunction: HotSwappableFunction<any> = ((...args: any[]) => {
return wrappedFunction[FUNCTION_CURRENT_IMPLEMENTATION_KEY](...args);
}) as any;
Object.defineProperty(
wrappedFunction,
FUNCTION_CURRENT_IMPLEMENTATION_KEY,
{
enumerable: false,
configurable: false,
writable: true,
value,
},
);
seen.set(value, wrappedFunction);
return wrappedFunction;
}
return value;
}
function collectNestedHotSwappableValues(
value: unknown,
seen: Set<any> = new Set(),
): HotSwappableFunction<any>[] | undefined {
if (seen.has(value)) return undefined;
seen.add(value);
if (Array.isArray(value)) {
return value.reduce<HotSwappableFunction<any>[]>((all, element) => {
const nested = collectNestedHotSwappableValues(element, seen);
return nested ? [...all, ...nested] : all;
}, []);
}
if (isBasicObject(value)) {
return Object.keys(value).reduce<HotSwappableFunction<any>[]>(
(all, key) => {
const nested = collectNestedHotSwappableValues(
(value as any)[key],
seen,
);
return nested ? [...all, ...nested] : all;
},
[],
);
}
if (typeof value === 'function') {
return FUNCTION_CURRENT_IMPLEMENTATION_KEY in value ? [value] : undefined;
}
return undefined;
}
function remove(child: AnyChild) {
(child.parent as AnyParent)?.removeChild(child);
}
function append(
container: AnyParent,
children: AnyChild[],
internals: ParentInternals,
rootInternals: RootInternals,
) {
for (const child of children) {
appendChild(container, child, internals, rootInternals);
}
}
function appendChild(
container: AnyParent,
child: AnyChild,
internals: ParentInternals,
rootInternals: RootInternals,
) {
const {nodes, strict} = rootInternals;
if (!nodes.has(child)) {
throw new Error(
`Cannot append a node that was not created by this remote root`,
);
}
const currentParent = child.parent;
const existingIndex = currentParent?.children.indexOf(child) ?? -1;
return perform(container, rootInternals, {
remote: (channel) => {
channel(
ACTION_INSERT_CHILD,
(container as any).id,
existingIndex < 0
? container.children.length
: container.children.length - 1,
serializeChild(child),
currentParent ? currentParent.id : false,
);
},
local: () => {
moveNodeToContainer(container, child, rootInternals);
let newChildren: AnyChild[];
if (currentParent) {
const currentInternals = getCurrentInternals(
currentParent,
rootInternals,
)!;
const currentChildren = [...currentInternals.children];
currentChildren.splice(existingIndex, 1);
if (currentParent === container) {
newChildren = currentChildren;
} else {
currentInternals.children = strict
? Object.freeze(currentChildren)
: currentChildren;
newChildren = [...internals.children];
}
} else {
newChildren = [...internals.children];
}
newChildren.push(child);
internals.children = strict ? Object.freeze(newChildren) : newChildren;
},
});
}
function replaceChildren(
container: AnyParent,
children: AnyChild[],
internals: ParentInternals,
rootInternals: RootInternals,
) {
for (const child of container.children) {
removeChild(container, child, internals, rootInternals);
}
append(container, children, internals, rootInternals);
}
// there is a problem with this, because when multiple children
// are removed, there is no guarantee the messages will arrive in the
// order we need them to on the host side (it depends how React
// calls our reconciler). If it calls with, for example, the removal of
// the second last item, then the removal of the last item, it will fail
// because the indexes moved around.
//
// Might need to send the removed child ID, or find out if we
// can collect removals into a single update.
function removeChild(
container: AnyParent,
child: AnyChild,
internals: ParentInternals,
rootInternals: RootInternals,
) {
const {strict} = rootInternals;
const childIndex = container.children.indexOf(child as any);
if (childIndex === -1) {
return undefined;
}
return perform(container, rootInternals, {
remote: (channel) =>
channel(ACTION_REMOVE_CHILD, (container as any).id, childIndex),
local: () => {
removeNodeFromContainer(child, rootInternals);
const newChildren = [...internals.children];
newChildren.splice(newChildren.indexOf(child), 1);
internals.children = strict ? Object.freeze(newChildren) : newChildren;
},
});
}
function insertBefore(
container: AnyParent,
child: AnyChild,
before: AnyChild | undefined | null,
internals: ParentInternals,
rootInternals: RootInternals,
) {
const {strict, nodes} = rootInternals;
if (!nodes.has(child)) {
throw new Error(
`Cannot insert a node that was not created by this remote root`,
);
}
const currentParent = child.parent;
const existingIndex = currentParent?.children.indexOf(child) ?? -1;
return perform(container, rootInternals, {
remote: (channel) => {
const beforeIndex =
before == null
? container.children.length - 1
: container.children.indexOf(before as any);
channel(
ACTION_INSERT_CHILD,
(container as any).id,
beforeIndex < existingIndex || existingIndex < 0
? beforeIndex
: beforeIndex - 1,
serializeChild(child),
currentParent ? currentParent.id : false,
);
},
local: () => {
moveNodeToContainer(container, child, rootInternals);
let newChildren: AnyChild[];
if (currentParent) {
const currentInternals = getCurrentInternals(
currentParent,
rootInternals,
)!;
const currentChildren = [...currentInternals.children];
currentChildren.splice(existingIndex, 1);
if (currentParent === container) {
newChildren = currentChildren;
} else {
currentInternals.children = strict
? Object.freeze(currentChildren)
: currentChildren;
newChildren = [...internals.children];
}
} else {
newChildren = [...internals.children];
}
if (before == null) {
newChildren.push(child);
} else {
newChildren.splice(newChildren.indexOf(before), 0, child);
}
internals.children = strict ? Object.freeze(newChildren) : newChildren;
},
});
}
function normalizeChild(
child: AnyChild | string,
root: RemoteRoot<any, any>,
): AnyChild {
return typeof child === 'string' ? root.createText(child) : child;
}
function moveNodeToContainer(
container: AnyParent,
node: AnyNode,
rootInternals: RootInternals,
) {
const {tops, parents} = rootInternals;
const newTop =
container.kind === KIND_ROOT ? container : tops.get(container)!;
tops.set(node, newTop);
parents.set(node, container);
moveFragmentToContainer(node, rootInternals);
allDescendants(node, (descendant) => {
tops.set(descendant, newTop);
moveFragmentToContainer(descendant, rootInternals);
});
}
function moveFragmentToContainer(node: AnyNode, rootInternals: RootInternals) {
if (node.kind !== KIND_COMPONENT) return;
const props = node.props as any;
if (!props) return;
Object.values(props).forEach((prop) => {
if (!isRemoteFragment(prop)) return;
moveNodeToContainer(node, prop, rootInternals);
});
}
function removeNodeFromContainer(node: AnyNode, rootInternals: RootInternals) {
const {tops, parents} = rootInternals;
tops.delete(node);
parents.delete(node);
allDescendants(node, (descendant) => {
tops.delete(descendant);
removeFragmentFromContainer(descendant, rootInternals);
});
removeFragmentFromContainer(node, rootInternals);
}
function removeFragmentFromContainer(
node: AnyNode,
rootInternals: RootInternals,
) {
if (node.kind !== KIND_COMPONENT) return;
const props = node.remoteProps as any;
for (const key of Object.keys(props ?? {})) {
const prop = props[key];
if (!isRemoteFragment(prop)) continue;
removeNodeFromContainer(prop, rootInternals);
}
}
function makePartOfTree(node: AnyNode, {parents, tops, nodes}: RootInternals) {
nodes.add(node);
Object.defineProperty(node, 'parent', {
get() {
return parents.get(node);
},
configurable: true,
enumerable: true,
});
Object.defineProperty(node, 'top', {
get() {
return tops.get(node);
},
configurable: true,
enumerable: true,
});
}
function serializeChild(value: AnyChild): Serialized<typeof value> {
return value.kind === KIND_TEXT
? {id: value.id, kind: value.kind, text: value.text}
: {
id: value.id,
kind: value.kind,
type: value.type,
props: value.remoteProps,
children: value.children.map((child) => serializeChild(child)),
};
}
function serializeProp(prop: any) {
if (isRemoteFragment(prop)) {
return serializeFragment(prop);
}
return prop;
}
function serializeFragment(
value: RemoteFragment<any>,
): RemoteFragmentSerialization {
return {
id: value.id,
kind: value.kind,
get children() {
return value.children.map((child) => serializeChild(child));
},
};
}
function getCurrentInternals(
currentParent: AnyChild['parent'],
rootInternals: RootInternals,
): ParentInternals | undefined {
if (currentParent.kind === KIND_ROOT) {
return rootInternals;
}
if (currentParent.kind === KIND_FRAGMENT) {
return rootInternals.fragments.get(currentParent);
}
return rootInternals.components.get(currentParent);
}
function makeRemote<Root extends RemoteRoot<any, any>>(
value: RemoteText<Root> | RemoteComponent<any, Root> | RemoteFragment<Root>,
id: string,
root: Root,
) {
Object.defineProperty(value, 'id', {
value: id,
configurable: true,
writable: false,
enumerable: false,
});
Object.defineProperty(value, 'root', {
value: root,
configurable: true,
writable: false,
enumerable: false,
});
}
function tryHotSwappingObjectValues(
currentValue: object,
newValue: unknown,
seen: Set<any>,
): HotSwapResult {
if (!isBasicObject(newValue)) {
return [
makeValueHotSwappable(newValue),
collectNestedHotSwappableValues(currentValue)?.map(
(hotSwappable) => [hotSwappable, undefined] as const,
),
];
}
let hasChanged = false;
const hotSwaps: HotSwapRecord[] = [];
const normalizedNewValue: {[key: string]: any} = {};
// eslint-disable-next-line guard-for-in
for (const key in currentValue) {
const currentObjectValue = (currentValue as any)[key];
if (!(key in newValue)) {
hasChanged = true;
const nestedHotSwappables =
collectNestedHotSwappableValues(currentObjectValue);
if (nestedHotSwappables) {
hotSwaps.push(
...nestedHotSwappables.map(
(hotSwappable) => [hotSwappable, undefined] as const,
),
);
}
}
const newObjectValue = (newValue as any)[key];
const [updatedValue, elementHotSwaps] = tryHotSwappingValues(
currentObjectValue,
newObjectValue,
seen,
);
if (elementHotSwaps) {
hotSwaps.push(...elementHotSwaps);
}
if (updatedValue !== IGNORE) {
hasChanged = true;
normalizedNewValue[key] = updatedValue;
}
}
for (const key in newValue) {
if (key in normalizedNewValue) continue;
hasChanged = true;
normalizedNewValue[key] = makeValueHotSwappable((newValue as any)[key]);
}
return [hasChanged ? normalizedNewValue : IGNORE, hotSwaps];
}
function tryHotSwappingArrayValues(
currentValue: unknown[],
newValue: unknown,
seen: Set<any>,
): HotSwapResult {
if (!Array.isArray(newValue)) {
return [
makeValueHotSwappable(newValue),
collectNestedHotSwappableValues(currentValue)?.map(
(hotSwappable) => [hotSwappable, undefined] as const,
),
];
}
let hasChanged = false;
const hotSwaps: HotSwapRecord[] = [];
const newLength = newValue.length;
const currentLength = currentValue.length;
const maxLength = Math.max(currentLength, newLength);
const normalizedNewValue: any[] = [];
for (let i = 0; i < maxLength; i++) {
const currentArrayValue = currentValue[i];
const newArrayValue = newValue[i];
if (i < newLength) {
if (i >= currentLength) {
hasChanged = true;
normalizedNewValue[i] = makeValueHotSwappable(newArrayValue);
continue;
}
const [updatedValue, elementHotSwaps] = tryHotSwappingValues(
currentArrayValue,
newArrayValue,
seen,
);
if (elementHotSwaps) hotSwaps.push(...elementHotSwaps);
if (updatedValue === IGNORE) {
normalizedNewValue[i] = currentArrayValue;
continue;
}
hasChanged = true;
normalizedNewValue[i] = updatedValue;
} else {
hasChanged = true;
const nestedHotSwappables =
collectNestedHotSwappableValues(currentArrayValue);
if (nestedHotSwappables) {
hotSwaps.push(
...nestedHotSwappables.map(
(hotSwappable) => [hotSwappable, undefined] as const,
),
);
}
}
}
return [hasChanged ? normalizedNewValue : IGNORE, hotSwaps];
}
import {
RemoteComponentType,
IdentifierForRemoteComponent,
PropsForRemoteComponent,
} from '@remote-ui/types';
type NonOptionalKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];
type IfAllOptionalKeys<Obj, If, Else = never> = Obj extends Record<
string,
never
>
? If
: NonOptionalKeys<Obj> extends {
length: 0;
}
? If
: Else;
export const ACTION_MOUNT = 0;
export const ACTION_INSERT_CHILD = 1;
export const ACTION_REMOVE_CHILD = 2;
export const ACTION_UPDATE_TEXT = 3;
export const ACTION_UPDATE_PROPS = 4;
export const UPDATE_INSERT = 0;
export const UPDATE_REMOVE = 1;
export const KIND_ROOT = 0;
export const KIND_COMPONENT = 1;
export const KIND_TEXT = 2;
export const KIND_FRAGMENT = 3;
export type Id = string;
export interface ActionArgumentMap {
[ACTION_MOUNT]: [(RemoteTextSerialization | RemoteComponentSerialization)[]];
[ACTION_INSERT_CHILD]: [
Id | undefined,
number,
RemoteTextSerialization | RemoteComponentSerialization,
Id | undefined | false,
];
[ACTION_REMOVE_CHILD]: [Id | undefined, number];
[ACTION_UPDATE_TEXT]: [Id, string];
[ACTION_UPDATE_PROPS]: [Id, Record<string, unknown>];
}
export interface RemoteChannel {
<T extends keyof ActionArgumentMap>(
type: T,
...payload: ActionArgumentMap[T]
): void | Promise<void>;
}
type AllowedRemoteChildren<
Children,
Root extends RemoteRoot<any, any>,
> = Children extends RemoteComponentType<string, any, any>
? RemoteComponent<Children, Root>
: never;
type ExtractChildren<Type> = Type extends RemoteComponentType<
string,
any,
infer Children
>
? Children
: never;
type AllowedChildren<
Children extends RemoteComponentType<string, any> | boolean,
Root extends RemoteRoot<any, any>,
AllowString extends boolean = false,
> = Children extends true
? RemoteComponent<any, Root> | AllowedTextChildren<Root, AllowString>
: Children extends false
? never
:
| AllowedRemoteChildren<Children, Root>
| AllowedTextChildren<Root, AllowString>;
type AllowedTextChildren<
Root extends RemoteRoot<any, any>,
AllowString extends boolean = false,
> = AllowString extends true ? RemoteText<Root> | string : RemoteText<Root>;
export interface RemoteRootOptions<
AllowedComponents extends RemoteComponentType<string, any>,
> {
readonly strict?: boolean;
readonly components?: ReadonlyArray<AllowedComponents>;
}
export interface RemoteRoot<
AllowedComponents extends RemoteComponentType<
string,
any
> = RemoteComponentType<any, any>,
AllowedChildrenTypes extends
| RemoteComponentType<string, any>
| boolean = true,
> {
readonly kind: typeof KIND_ROOT;
readonly children: ReadonlyArray<
AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>
>
>;
readonly options: RemoteRootOptions<AllowedComponents>;
append(
...children: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>,
true
>[]
): void | Promise<void>;
/**
* @deprecated use `RemoteRoot.append` instead.
*/
appendChild(
child: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>,
true
>,
): void | Promise<void>;
removeChild(
child: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>
>,
): void | Promise<void>;
replaceChildren(
...children: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>,
true
>[]
): void | Promise<void>;
insertBefore(
child: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>
>,
before?: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>
> | null,
): void | Promise<void>;
/**
* @deprecated use `RemoteRoot.insertBefore` instead.
*/
insertChildBefore(
child: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>
>,
before: AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>
>,
): void | Promise<void>;
createComponent<Type extends AllowedComponents>(
type: Type,
...rest: IfAllOptionalKeys<
PropsForRemoteComponent<Type>,
| [
(PropsForRemoteComponent<Type> | null)?,
...AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>,
true
>[],
]
| [
(PropsForRemoteComponent<Type> | null)?,
AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>,
true
>[]?,
],
| [
PropsForRemoteComponent<Type>,
...AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>,
true
>[],
]
| [
PropsForRemoteComponent<Type>,
AllowedChildren<
AllowedChildrenTypes,
RemoteRoot<AllowedComponents, AllowedChildrenTypes>,
true
>[]?,
]
>
): RemoteComponent<Type, RemoteRoot<AllowedComponents, AllowedChildrenTypes>>;
createText(
text?: string,
): RemoteText<RemoteRoot<AllowedComponents, AllowedChildrenTypes>>;
createFragment(): RemoteFragment<
RemoteRoot<AllowedComponents, AllowedChildrenTypes>
>;
mount(): Promise<void>;
}
export interface RemoteComponent<
Type extends RemoteComponentType<string, any>,
Root extends RemoteRoot<any, any>,
> {
readonly kind: typeof KIND_COMPONENT;
readonly id: string;
readonly type: IdentifierForRemoteComponent<Type>;
readonly props: PropsForRemoteComponent<Type>;
readonly remoteProps: PropsForRemoteComponent<Type>;
readonly children: ReadonlyArray<
AllowedChildren<ExtractChildren<Type>, Root>
>;
readonly root: Root;
readonly top: RemoteComponent<any, Root> | Root | null;
readonly parent: RemoteComponent<any, Root> | Root | null;
remove(): void | Promise<void>;
updateProps(
props: Partial<PropsForRemoteComponent<Type>>,
): void | Promise<void>;
append(
...children: AllowedChildren<ExtractChildren<Type>, Root, true>[]
): void | Promise<void>;
/**
* @deprecated use `RemoteComponent.append` instead.
*/
appendChild(
child: AllowedChildren<ExtractChildren<Type>, Root, true>,
): void | Promise<void>;
removeChild(
child: AllowedChildren<ExtractChildren<Type>, Root>,
): void | Promise<void>;
replaceChildren(
...children: AllowedChildren<ExtractChildren<Type>, Root, true>[]
): void | Promise<void>;
insertBefore(
child: AllowedChildren<ExtractChildren<Type>, Root>,
before?: AllowedChildren<ExtractChildren<Type>, Root> | null,
): void | Promise<void>;
/**
* @deprecated use `RemoteComponent.insertBefore` instead.
*/
insertChildBefore(
child: AllowedChildren<ExtractChildren<Type>, Root>,
before: AllowedChildren<ExtractChildren<Type>, Root>,
): void | Promise<void>;
}
export interface RemoteFragment<
Root extends RemoteRoot<any, any> = RemoteRoot<any, any>,
> {
readonly kind: typeof KIND_FRAGMENT;
readonly id: string;
readonly children: ReadonlyArray<AllowedChildren<ExtractChildren<any>, Root>>;
readonly root: Root;
readonly top: RemoteComponent<any, Root> | Root | null;
readonly parent: RemoteComponent<any, Root> | Root | null;
append(
...children: AllowedChildren<ExtractChildren<any>, Root, true>[]
): void | Promise<void>;
/**
* @deprecated use `RemoteComponent.append` instead.
*/
appendChild(
child: AllowedChildren<ExtractChildren<any>, Root, true>,
): void | Promise<void>;
removeChild(
child: AllowedChildren<ExtractChildren<any>, Root>,
): void | Promise<void>;
replaceChildren(
...children: AllowedChildren<ExtractChildren<any>, Root, true>[]
): void | Promise<void>;
insertBefore(
child: AllowedChildren<ExtractChildren<any>, Root>,
before?: AllowedChildren<ExtractChildren<any>, Root> | null,
): void | Promise<void>;
/**
* @deprecated use `RemoteComponent.insertBefore` instead.
*/
insertChildBefore(
child: AllowedChildren<ExtractChildren<any>, Root>,
before: AllowedChildren<ExtractChildren<any>, Root>,
): void | Promise<void>;
}
export interface RemoteText<Root extends RemoteRoot<any, any>> {
readonly kind: typeof KIND_TEXT;
readonly id: string;
readonly text: string;
readonly root: Root;
readonly top: RemoteComponent<any, Root> | Root | null;
readonly parent: RemoteComponent<any, Root> | Root | null;
update(text: string): void | Promise<void>;
/**
* @deprecated use `RemoteText.update` instead.
*/
updateText(text: string): void | Promise<void>;
remove(): void | Promise<void>;
}
export type RemoteChild<Root extends RemoteRoot<any, any>> =
| RemoteComponent<any, Root>
| RemoteText<Root>;
export type RemoteComponentSerialization<
Type extends RemoteComponentType<string, any> = RemoteComponentType<
string,
any
>,
> = {
-readonly [K in 'id' | 'type' | 'kind' | 'props']: RemoteComponent<
Type,
any
>[K];
} & {
children: (RemoteComponentSerialization | RemoteTextSerialization)[];
};
export type RemoteTextSerialization = {
-readonly [K in 'id' | 'text' | 'kind']: RemoteText<any>[K];
};
export type RemoteFragmentSerialization = {
-readonly [K in 'id' | 'kind']: RemoteFragment<any>[K];
} & {
children: (RemoteComponentSerialization | RemoteTextSerialization)[];
};
export type Serialized<T> = T extends RemoteComponent<infer Type, any>
? RemoteComponentSerialization<Type>
: T extends RemoteText<any>
? RemoteTextSerialization
: never;
import type {RemoteComponentType} from '@remote-ui/types';
import type {
RemoteRoot,
RemoteComponent,
RemoteText,
RemoteFragment,
} from './types';
import {KIND_COMPONENT, KIND_TEXT, KIND_FRAGMENT} from './types';
export function isRemoteComponent<
Type extends RemoteComponentType<string, any, any> = any,
Root extends RemoteRoot<any, any> = RemoteRoot<any, any>,
>(child: unknown): child is RemoteComponent<Type, Root> {
return child != null && (child as any).kind === KIND_COMPONENT;
}
export function isRemoteText<
Root extends RemoteRoot<any, any> = RemoteRoot<any, any>,
>(child: unknown): child is RemoteText<Root> {
return child != null && (child as any).kind === KIND_TEXT;
}
export function isRemoteFragment<
Root extends RemoteRoot<any, any> = RemoteRoot<any, any>,
>(object: unknown): object is RemoteFragment<Root> {
return object != null && (object as any).kind === KIND_FRAGMENT;
}
import {createPackage, Runtime} from '@shopify/loom';
import {defaultProjectPlugin} from '../../config/loom';
export default createPackage((pkg) => {
pkg.runtimes(Runtime.Node, Runtime.Browser);
pkg.entry({root: './src/index.ts'});
pkg.entry({root: './src/host/index.ts', name: 'host'});
pkg.use(defaultProjectPlugin({react: true}));
});
{
"name": "@remote-ui/react",
"version": "5.0.8",
"publishConfig": {
"access": "public",
"@remote-ui:registry": "https://registry.npmjs.org"
},
"license": "MIT",
"main": "index.js",
"module": "index.mjs",
"esnext": "index.esnext",
"types": "./build/ts/index.d.ts",
"typesVersions": {
"*": {
"host": [
"./build/ts/host/index.d.ts"
]
}
},
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
},
"./host": {
"types": "./build/ts/host/index.d.ts",
"esnext": "./host.esnext",
"import": "./host.mjs",
"require": "./host.js"
}
},
"sideEffects": false,
"devDependencies": {
"@types/react-dom": "^18.0.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-reconciler": "^0.29.0"
},
"dependencies": {
"@remote-ui/async-subscription": "^2.1.18",
"@remote-ui/core": "^2.2.7",
"@remote-ui/rpc": "^1.4.7",
"@types/react": ">=17.0.0 <19.0.0",
"@types/react-reconciler": ">=0.26.0 <0.30.0"
},
"peerDependencies": {
"react": ">=17.0.0 <19.0.0",
"react-reconciler": ">=0.26.0 <0.30.0"
},
"peerDependenciesMeta": {
"react": {
"optional": false
},
"react-reconciler": {
"optional": true
}
}
}
import {createContext} from 'react';
import type {RemoteRoot} from '@remote-ui/core';
import type {Reconciler} from './reconciler';
export interface RenderContextDescriptor {
root: RemoteRoot;
reconciler: Reconciler;
}
export const RenderContext = createContext<RenderContextDescriptor | null>(
null,
);
export {useRemoteSubscription} from './subscription';
export {useRender} from './render';
import {useContext} from 'react';
import {RenderContext} from '../context';
export function useRender() {
const render = useContext(RenderContext);
if (render == null) {
throw new Error('No remote-ui Render instance found in context');
}
return render;
}
import {useState, useEffect, useDebugValue} from 'react';
import type {StatefulRemoteSubscribable} from '@remote-ui/async-subscription';
export function useRemoteSubscription<T>(
subscribable: StatefulRemoteSubscribable<T>,
) {
const [state, setState] = useState(() => ({
subscribable,
value: subscribable.current,
}));
let valueToReturn = state.value;
if (state.subscribable !== subscribable) {
valueToReturn = subscribable.current;
setState({subscribable, value: valueToReturn});
}
useDebugValue(valueToReturn);
useEffect(() => {
const unsubscribe = subscribable.subscribe(checkForUpdates);
checkForUpdates();
return () => {
unsubscribe();
};
function checkForUpdates() {
const value = subscribable.current;
setState((previousState) => {
if (
previousState.subscribable !== subscribable ||
previousState.value === value
) {
return previousState;
}
return {...previousState, value};
});
}
}, [subscribable]);
return valueToReturn;
}
import {useState, useDebugValue, useEffect} from 'react';
import type {RemoteReceiver, RemoteReceiverAttachable} from '@remote-ui/core';
interface State<T extends RemoteReceiverAttachable> {
receiver: RemoteReceiver;
id: RemoteReceiverAttachable['id'];
version?: RemoteReceiverAttachable['version'];
value: T | null;
}
export function useAttached<T extends RemoteReceiverAttachable>(
receiver: RemoteReceiver,
attached: T,
) {
const [state, setState] = useState<State<T>>({
receiver,
id: attached.id,
version: attached.version,
value: attached,
});
let returnValue: T | null = state.value;
// If parameters have changed since our last render, schedule an update with its current value.
if (state.receiver !== receiver || state.id !== attached.id) {
// When the consumer of this hook changes receiver or attached node, the node they switched
// to might already be unmounted. We guard against that by making sure we don’t get null
// back from the receiver, and storing the “attached” node in state whether it is actually
// attached or not, so we have a paper trail of how we got here.
const updated = receiver.attached.get<T>(attached);
// If the subscription has been updated, we'll schedule another update with React.
// React will process this update immediately, so the old subscription value won't be committed.
// It is still nice to avoid returning a mismatched value though, so let's override the return value.
returnValue = updated;
setState({
receiver,
id: attached.id,
version: attached.version,
value: returnValue,
});
}
useDebugValue(returnValue);
useEffect(() => {
let didUnsubscribe = false;
const checkForUpdates = () => {
if (didUnsubscribe) return;
setState((previousState) => {
const {
id: previousId,
version: previousVersion,
receiver: previousReceiver,
} = previousState;
const {id} = attached;
// Ignore values from stale sources
if (previousReceiver !== receiver || previousId !== id) {
return previousState;
}
// This function is also called as part of the initial useEffect() when the
// component mounts. It’s possible that between the initial render (when the
// remote component was for sure attached, to the best of the host’s knowledge)
// and the effect, the component was removed from the remote tree. You’ll see that
// the rest of this callback is careful to handle cases where the node is in this
// state.
const value = receiver.attached.get<T>(attached);
const version = value?.version;
// If the value hasn't changed, no update is needed.
// Return state as-is so React can bail out and avoid an unnecessary render.
if (previousVersion === version) {
return previousState;
}
return {receiver, value, id, version};
});
};
const unsubscribe = receiver.attached.subscribe(attached, checkForUpdates);
// Passive effect, so we need to check if anything has changed
checkForUpdates();
return () => {
didUnsubscribe = true;
unsubscribe();
};
}, [receiver, attached]);
return returnValue;
}
export type {RemoteReceiver} from '@remote-ui/core';
export {createRemoteReceiver} from '@remote-ui/core';
export {RemoteRenderer} from './RemoteRenderer';
export type {RemoteRendererProps} from './RemoteRenderer';
export {RemoteComponent} from './RemoteComponent';
export {RemoteText} from './RemoteText';
export {createController} from './controller';
export {useAttached} from './hooks';
export type {ComponentMapping, ControllerOptions} from './controller';
export type {
ReactPropsFromRemoteComponentType,
ReactComponentTypeFromRemoteComponentType,
} from '../types';
export type {Controller, RemoteComponentProps, RemoteTextProps} from './types';
import type {ComponentType, ReactNode} from 'react';
import type {
RemoteReceiver,
RemoteReceiverAttachableRoot,
RemoteReceiverAttachableComponent,
RemoteReceiverAttachableText,
RemoteComponentType,
RemoteReceiverAttachableFragment,
} from '@remote-ui/core';
export interface RemoteTextProps {
parent: RemoteReceiverAttachableRoot | RemoteReceiverAttachableComponent;
text: RemoteReceiverAttachableText;
receiver: RemoteReceiver;
key: string | number;
}
export interface RemoteComponentProps {
receiver: RemoteReceiver;
parent: RemoteReceiverAttachableRoot | RemoteReceiverAttachableComponent;
component: RemoteReceiverAttachableComponent;
controller: Controller;
key: string | number;
}
export interface RemoteFragmentProps {
receiver: RemoteReceiver;
parent: RemoteReceiverAttachableComponent;
fragment: RemoteReceiverAttachableFragment;
controller: Controller;
}
export interface Controller {
get(type: string | RemoteComponentType<string, any, any>): ComponentType<any>;
renderer: Renderer;
}
export interface Renderer {
renderComponent(props: RemoteComponentProps): ReactNode;
renderText(props: RemoteTextProps): ReactNode;
}
export interface RenderComponentOptions {
renderDefault(): ReactNode;
}
export interface RenderTextOptions {
renderDefault(): ReactNode;
}
export {createRemoteRoot} from '@remote-ui/core';
export type {RemoteRoot, RemoteReceiver} from '@remote-ui/core';
export {render, createRoot} from './render';
export type {Root} from './render';
export {createRemoteReactComponent} from './components';
export {useRemoteSubscription} from './hooks';
export type {
ReactPropsFromRemoteComponentType,
ReactComponentTypeFromRemoteComponentType,
} from './types';
export {createReconciler} from './reconciler';
export type {Reconciler} from './reconciler';
import reactReconciler from 'react-reconciler';
import type {Reconciler as ReactReconciler} from 'react-reconciler';
import type {
RemoteRoot,
RemoteText,
RemoteComponent,
RemoteComponentType,
} from '@remote-ui/core';
type Type = RemoteComponentType<string, any>;
type Props = Record<string, unknown>;
type ViewInstance = RemoteComponent<any, any>;
type TextInstance = RemoteText<any>;
type HostContext = Record<string, never>;
type UpdatePayload = Record<string, unknown>;
type SuspenseInstance = never;
type PublicInstance = unknown;
type HydratableInstance = unknown;
type ChildSet = unknown;
type TimeoutHandle = unknown;
type NoTimeout = unknown;
export type Reconciler = ReactReconciler<
RemoteRoot,
ViewInstance,
TextInstance,
SuspenseInstance,
PublicInstance
>;
export const createReconciler = (options?: {primary?: boolean}) =>
reactReconciler<
Type,
Props,
RemoteRoot,
ViewInstance,
TextInstance,
SuspenseInstance,
HydratableInstance,
PublicInstance,
HostContext,
UpdatePayload,
ChildSet,
TimeoutHandle,
NoTimeout
>({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Compat for React <= 17.x
now: Date.now,
// Timeout
scheduleTimeout: setTimeout,
cancelTimeout: clearTimeout,
noTimeout: false,
// Microtask scheduling
// @see https://github.com/facebook/react/blob/2c8a1452b82b9ec5ebfa3f370b31fda19610ae92/packages/react-dom/src/client/ReactDOMHostConfig.js#L391-L401
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - types in `@types/react-reconciler` are outdated
supportsMicrotasks: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - types in `@types/react-reconciler` are outdated
scheduleMicrotask,
// Compat for React <= 17.x
queueMicrotask: scheduleMicrotask,
isPrimaryRenderer: options?.primary ?? true,
supportsMutation: true,
supportsHydration: false,
supportsPersistence: false,
// Context
getRootHostContext() {
return {};
},
getChildHostContext(context) {
return context;
},
// Instances
createTextInstance(text, root) {
return root.createText(text);
},
createInstance(type, allProps, root) {
const {children: _children, ...props} = allProps;
return root.createComponent(type, props);
},
// Updates
commitTextUpdate(text, _oldText, newText) {
text.update(newText);
},
prepareUpdate(_instance, _type, oldProps, newProps) {
const updateProps: Record<string, unknown> = {};
let needsUpdate = false;
for (const key in oldProps) {
if (!has(oldProps, key) || key === 'children') {
continue;
}
if (!(key in newProps)) {
needsUpdate = true;
updateProps[key] = undefined;
// } else if (typeof oldProps[key] === 'function') {
// if (typeof newProps[key] === 'function') {
// fragment.controller.functions.exchange(
// oldProps[key] as Function,
// newProps[key] as Function,
// );
// } else {
// needsUpdate = true;
// fragment.controller.functions.revoke(oldProps[key] as Function);
// updateProps[key] = newProps[key];
// }
} else if (oldProps[key] !== newProps[key]) {
needsUpdate = true;
updateProps[key] = newProps[key];
}
}
for (const key in newProps) {
if (!has(newProps, key) || key === 'children') {
continue;
}
if (!(key in oldProps)) {
needsUpdate = true;
updateProps[key] = newProps[key];
}
}
return needsUpdate ? updateProps : null;
},
commitUpdate(instance, payload) {
instance.updateProps(payload);
},
// Update root
appendChildToContainer(remoteRoot, child) {
remoteRoot.append(child);
},
insertInContainerBefore(remoteRoot, child, beforeChild) {
remoteRoot.insertBefore(child, beforeChild);
},
removeChildFromContainer(remoteRoot, child) {
remoteRoot.removeChild(child);
},
clearContainer(remoteRoot) {
for (const child of remoteRoot.children) {
remoteRoot.removeChild(child);
}
},
// Update children
appendInitialChild(parent, child) {
parent.append(child);
},
appendChild(parent, child) {
parent.append(child);
},
insertBefore(parent, newChild, beforeChild) {
parent.insertBefore(newChild, beforeChild);
},
removeChild(parent, child) {
parent.removeChild(child);
},
// Unknown
finalizeInitialChildren() {
return false;
},
shouldSetTextContent() {
return false;
},
getPublicInstance() {},
prepareForCommit() {
return null;
},
resetAfterCommit() {},
commitMount() {},
preparePortalMount() {},
detachDeletedInstance() {},
});
function scheduleMicrotask(callback: () => void) {
return typeof queueMicrotask === 'function'
? queueMicrotask
: Promise.resolve(null).then(callback).catch(handleErrorInNextTick);
}
function handleErrorInNextTick(error: Error) {
setTimeout(() => {
throw error;
});
}
const {hasOwnProperty} = {};
function has(object: object, property: string | number | symbol) {
return hasOwnProperty.call(object, property);
}
import type {ReactNode, ComponentType, ReactElement} from 'react';
import type {RemoteComponentType, RemoteFragment} from '@remote-ui/core';
type PropsForRemoteComponent<T> = T extends RemoteComponentType<
string,
infer Props,
any
>
? Props extends Record<string, never>
? // eslint-disable-next-line @typescript-eslint/ban-types
{}
: {[K in keyof Props]: RemoteFragmentToReactElement<Props[K]>}
: never;
type RemoteFragmentToReactElement<T> = T extends RemoteFragment<infer R>
? ReactElement | false | RemoteFragment<R>
: T;
/* eslint-disable @typescript-eslint/ban-types */
export type ReactPropsFromRemoteComponentType<
Type extends RemoteComponentType<string, any, any>,
> = PropsForRemoteComponent<Type> &
(Type extends RemoteComponentType<string, any, infer Children>
? false extends Children
? {}
: {
children?: ReactNode;
}
: {});
/* eslint-enable @typescript-eslint/ban-types */
export type ReactComponentTypeFromRemoteComponentType<
Type extends RemoteComponentType<string, any, any>,
> = ComponentType<ReactPropsFromRemoteComponentType<Type>>;
import {createPackage, Runtime} from '@shopify/loom';
import {defaultProjectPlugin} from '../../config/loom';
export default createPackage((pkg) => {
pkg.runtimes(Runtime.Node, Runtime.Browser);
pkg.entry({root: './src/index.ts'});
pkg.use(defaultProjectPlugin());
});
{
"name": "@remote-ui/rpc",
"description": "An RPC library with strong support for simulating the transfer of functions via postMessage",
"version": "1.4.7",
"publishConfig": {
"access": "public"
},
"license": "MIT",
"main": "index.js",
"module": "index.mjs",
"esnext": "index.esnext",
"types": "./build/ts/index.d.ts",
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
}
},
"sideEffects": false,
"devDependencies": {
"@types/uuid": "^8.0.0",
"uuid": "^8.2.0"
}
}
export const READY_MESSAGE_KEY = 'remote-ui::ready';
import type {MessageEndpoint} from '../types';
import {READY_MESSAGE_KEY} from './constants';
export function fromInsideIframe({targetOrigin = '*'} = {}): MessageEndpoint {
if (typeof self === 'undefined' || self.parent == null) {
throw new Error(
`This does not appear to be a child iframe, because there is no parent window.`,
);
}
// We wait until the document is ready before advertising to the parent that
// communication can commence.
// However, it's possible that the parent isn't listening to messages at this time.
// Which can lead to communication never starting.
// Therefore we also wait for the parent to send a message once it's ready to (re)send the
// ready message from within the child iframe.
const {parent} = self;
const ready = () => parent.postMessage(READY_MESSAGE_KEY, targetOrigin);
window.addEventListener('message', (event) => {
if (event.source !== parent || document.readyState !== 'complete') {
return;
}
if (event.data === READY_MESSAGE_KEY) {
ready();
}
});
if (document.readyState === 'complete') {
ready();
} else {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
ready();
}
});
}
// We need to store the listener, because we wrap it to do some origin checking. Ideally,
// we’d instead store an `AbortController`, and use its signal to cancel the listeners,
// but that isn’t widely supported.
const listenerMap = new WeakMap<
(event: MessageEvent) => void,
(event: MessageEvent) => void
>();
return {
postMessage(message, transfer) {
parent.postMessage(message, targetOrigin, transfer);
},
addEventListener(event, listener) {
const wrappedListener = (event: MessageEvent) => {
if (event.source !== parent) return;
listener(event);
};
listenerMap.set(listener, wrappedListener);
self.addEventListener(event, wrappedListener);
},
removeEventListener(event, listener) {
const wrappedListener = listenerMap.get(listener);
if (wrappedListener == null) return;
listenerMap.delete(listener);
self.removeEventListener(event, wrappedListener);
},
};
}
import type {MessageEndpoint} from '../types';
import {READY_MESSAGE_KEY} from './constants';
export function fromIframe(
target: HTMLIFrameElement,
{terminate: shouldTerminate = true, targetOrigin = '*'} = {},
): MessageEndpoint {
if (typeof window === 'undefined') {
throw new Error(
`You can only run fromIframe() in a browser context, but no window was found.`,
);
}
// We need to store the listener, because we wrap it to do some origin checking. Ideally,
// we’d instead store an `AbortController`, and use its signal to cancel the listeners,
// but that isn’t widely supported.
const listenerMap = new WeakMap<
(event: MessageEvent) => void,
(event: MessageEvent) => void
>();
let resolveIFrameReadyPromise: () => void;
function onMessage(event: MessageEvent<any>) {
if (event.source !== target.contentWindow) return;
if (event.data === READY_MESSAGE_KEY) {
window.removeEventListener('message', onMessage);
resolveIFrameReadyPromise();
}
}
target.contentWindow?.postMessage(READY_MESSAGE_KEY, targetOrigin);
const iframeReadyPromise = new Promise<void>((resolve) => {
resolveIFrameReadyPromise = resolve;
window.addEventListener('message', onMessage);
});
return {
async postMessage(message, transfer) {
await iframeReadyPromise;
target.contentWindow?.postMessage(message, targetOrigin, transfer);
},
addEventListener(event, listener) {
const wrappedListener = (event: MessageEvent) => {
if (event.source !== target.contentWindow) return;
listener(event);
};
listenerMap.set(listener, wrappedListener);
self.addEventListener(event, wrappedListener);
},
removeEventListener(event, listener) {
const wrappedListener = listenerMap.get(listener);
if (wrappedListener == null) return;
listenerMap.delete(listener);
self.removeEventListener(event, wrappedListener);
},
terminate() {
window.removeEventListener('message', onMessage);
if (shouldTerminate) target.remove();
},
};
}
export {fromIframe} from './iframe-parent';
export {fromInsideIframe} from './iframe-child';
export {fromMessagePort} from './message-port';
export {fromWebWorker} from './web-worker';
import type {MessageEndpoint} from '../types';
export function fromMessagePort(messagePort: MessagePort): MessageEndpoint {
return {
postMessage: (...args: [any, Transferable[]]) =>
messagePort.postMessage(...args),
addEventListener: (...args) => messagePort.addEventListener(...args),
removeEventListener: (...args) => messagePort.removeEventListener(...args),
terminate() {
messagePort.close();
},
};
}
import type {MessageEndpoint} from '../types';
export function fromWebWorker(worker: Worker): MessageEndpoint {
return worker;
}
import {
RETAINED_BY,
RETAIN_METHOD,
RELEASE_METHOD,
EncodingStrategy,
EncodingStrategyApi,
} from '../types';
import type {Retainer} from '../memory';
import {StackFrame, isBasicObject, isMemoryManageable} from '../memory';
type AnyFunction = (...args: any[]) => any;
const FUNCTION = '_@f';
export function createBasicEncoder(api: EncodingStrategyApi): EncodingStrategy {
const functionsToId = new Map<AnyFunction, string>();
const idsToFunction = new Map<string, AnyFunction>();
const idsToProxy = new Map<string, AnyFunction>();
return {
encode,
decode,
async call(id, args) {
const stackFrame = new StackFrame();
const func = idsToFunction.get(id);
if (func == null) {
throw new Error(
'You attempted to call a function that was already released.',
);
}
try {
const retainedBy = isMemoryManageable(func)
? [stackFrame, ...func[RETAINED_BY]]
: [stackFrame];
const result = await func(...(decode(args, retainedBy) as any[]));
return result;
} finally {
stackFrame.release();
}
},
release(id) {
const func = idsToFunction.get(id);
if (func) {
idsToFunction.delete(id);
functionsToId.delete(func);
}
},
terminate() {
functionsToId.clear();
idsToFunction.clear();
idsToProxy.clear();
},
};
type EncodeResult = [any, Transferable[]?];
function encode(
value: unknown,
seen = new Map<any, EncodeResult>(),
): EncodeResult {
if (value == null) {
return [value];
}
const seenValue = seen.get(value);
if (seenValue) {
return seenValue;
}
if (typeof value === 'object') {
if (Array.isArray(value)) {
seen.set(value, [undefined]);
const transferables: Transferable[] = [];
const result = value.map((item) => {
const [result, nestedTransferables = []] = encode(item, seen);
transferables.push(...nestedTransferables);
return result;
});
const fullResult: EncodeResult = [result, transferables];
seen.set(value, fullResult);
return fullResult;
}
if (isBasicObject(value)) {
seen.set(value, [undefined]);
const transferables: Transferable[] = [];
const result = Object.keys(value).reduce((object, key) => {
const [result, nestedTransferables = []] = encode(
(value as any)[key],
seen,
);
transferables.push(...nestedTransferables);
return {...object, [key]: result};
}, {});
const fullResult: EncodeResult = [result, transferables];
seen.set(value, fullResult);
return fullResult;
}
}
if (typeof value === 'function') {
if (functionsToId.has(value as AnyFunction)) {
const id = functionsToId.get(value as AnyFunction)!;
const result: EncodeResult = [{[FUNCTION]: id}];
seen.set(value, result);
return result;
}
const id = api.uuid();
functionsToId.set(value as AnyFunction, id);
idsToFunction.set(id, value as AnyFunction);
const result: EncodeResult = [{[FUNCTION]: id}];
seen.set(value, result);
return result;
}
const result: EncodeResult = [value];
seen.set(value, result);
return result;
}
function decode(value: unknown, retainedBy?: Iterable<Retainer>): any {
if (typeof value === 'object') {
if (value == null) {
return value as any;
}
if (Array.isArray(value)) {
return value.map((value) => decode(value, retainedBy));
}
if (FUNCTION in value) {
const id = (value as {[FUNCTION]: string})[FUNCTION];
if (idsToProxy.has(id)) {
return idsToProxy.get(id)! as any;
}
let retainCount = 0;
let released = false;
const release = () => {
retainCount -= 1;
if (retainCount === 0) {
released = true;
idsToProxy.delete(id);
api.release(id);
}
};
const retain = () => {
retainCount += 1;
};
const retainers = new Set(retainedBy);
const proxy = (...args: any[]) => {
if (released) {
throw new Error(
'You attempted to call a function that was already released.',
);
}
if (!idsToProxy.has(id)) {
throw new Error(
'You attempted to call a function that was already revoked.',
);
}
return api.call(id, args);
};
Object.defineProperties(proxy, {
[RELEASE_METHOD]: {value: release, writable: false},
[RETAIN_METHOD]: {value: retain, writable: false},
[RETAINED_BY]: {value: retainers, writable: false},
});
for (const retainer of retainers) {
retainer.add(proxy as any);
}
idsToProxy.set(id, proxy);
return proxy as any;
}
if (isBasicObject(value)) {
return Object.keys(value).reduce(
(object, key) => ({
...object,
[key]: decode((value as any)[key], retainedBy),
}),
{},
) as any;
}
}
return value as any;
}
}
export {createBasicEncoder} from './basic';
import {createBasicEncoder} from './encoding';
import type {
MessageEndpoint,
RemoteCallable,
EncodingStrategy,
EncodingStrategyApi,
} from './types';
import {StackFrame} from './memory';
import type {Retainer} from './memory';
export const CALL = 0;
export const RESULT = 1;
export const TERMINATE = 2;
export const RELEASE = 3;
export const FUNCTION_APPLY = 5;
export const FUNCTION_RESULT = 6;
type AnyFunction = (...args: any[]) => any;
interface MessageMap {
[CALL]: [string, string | number, any];
[RESULT]: [string, Error?, any?];
[TERMINATE]: void;
[RELEASE]: [string];
[FUNCTION_APPLY]: [string, string, any];
[FUNCTION_RESULT]: [string, Error?, any?];
}
type MessageData = {
[K in keyof MessageMap]: [K, MessageMap[K]];
}[keyof MessageMap];
export interface CreateEndpointOptions<T = unknown> {
uuid?(): string;
createEncoder?(api: EncodingStrategyApi): EncodingStrategy;
callable?: (keyof T)[];
}
export interface Endpoint<T> {
readonly call: RemoteCallable<T>;
replace(messenger: MessageEndpoint): void;
expose(api: Record<string, AnyFunction | undefined>): void;
callable(...methods: string[]): void;
terminate(): void;
}
export class MissingResolverError extends Error {
readonly callId: string;
readonly error?: Error;
readonly result?: unknown;
readonly groupingHash: string = 'RemoteUI::MissingResolverError';
constructor(message: {callId: string; error?: Error; result?: unknown}) {
const {callId, error, result} = message;
const errorMessage = error ? ` Error: ${String(error)}` : '';
const resultMessage =
result == null ? '' : ` Result: ${JSON.stringify(result)}`;
super(
`No resolver found for call ID: ${callId}${errorMessage}${resultMessage}`,
);
this.name = 'MissingResolverError';
this.callId = callId;
this.error = error;
this.result = result;
}
}
/**
* An endpoint wraps around a messenger, acting as the intermediary for all
* messages both send from, and received by, that messenger. The endpoint sends
* all messages as arrays, where the first element is the message type, and the
* second is the arguments for that message (as an array). For messages that send
* meaningful content across the wire (e.g., arguments to function calls, return
* results), the endpoint first encodes these values.
*
* Encoding is done using a CBOR-like encoding scheme. The value is encoded into
* an array buffer, and is paired with an additional array buffer that contains all
* the strings used in that message (in the encoded value, strings are encoded as
* their index in the "strings" encoding to reduce the cost of heavily-duplicated
* strings, which is more likely in payloads containing UI). This encoding also takes
* care of encoding functions: it uses a "tagged" item in CBOR to represent a
* function as a string ID, which the opposite endpoint will be capable of turning
* into a consistent, memory-manageable function proxy.
*
* The main CBOR encoding is entirely take from the [cbor.js package](https://github.com/paroga/cbor-js).
* The special behavior for encoding strings and functions was then added in to the
* encoder and decoder. For additional details on CBOR:
*
* @see https://tools.ietf.org/html/rfc7049
*/
export function createEndpoint<T>(
initialMessenger: MessageEndpoint,
{
uuid = defaultUuid,
createEncoder = createBasicEncoder,
callable,
}: CreateEndpointOptions<T> = {},
): Endpoint<T> {
let terminated = false;
let messenger = initialMessenger;
const activeApi = new Map<string | number, AnyFunction>();
const callIdsToResolver = new Map<
string,
(
...args: MessageMap[typeof FUNCTION_RESULT] | MessageMap[typeof RESULT]
) => void
>();
const call = createCallable<T>(handlerForCall, callable);
const encoder = createEncoder({
uuid,
release(id) {
send(RELEASE, [id]);
},
call(id, args, retainedBy) {
const callId = uuid();
const done = waitForResult(callId, retainedBy);
const [encoded, transferables] = encoder.encode(args);
send(FUNCTION_APPLY, [callId, id, encoded], transferables);
return done;
},
});
messenger.addEventListener('message', listener);
return {
call,
replace(newMessenger) {
const oldMessenger = messenger;
messenger = newMessenger;
oldMessenger.removeEventListener('message', listener);
newMessenger.addEventListener('message', listener);
},
expose(api) {
for (const key of Object.keys(api)) {
const value = api[key];
if (typeof value === 'function') {
activeApi.set(key, value);
} else {
activeApi.delete(key);
}
}
},
callable(...newCallable) {
// If no callable methods are supplied initially, we use a Proxy instead,
// so all methods end up being treated as callable by default.
if (callable == null) return;
for (const method of newCallable) {
Object.defineProperty(call, method, {
value: handlerForCall(method),
writable: false,
configurable: true,
enumerable: true,
});
}
},
terminate() {
send(TERMINATE, undefined);
terminate();
if (messenger.terminate) {
messenger.terminate();
}
},
};
function send<Type extends keyof MessageMap>(
type: Type,
args: MessageMap[Type],
transferables?: Transferable[],
) {
if (terminated) {
return;
}
messenger.postMessage(args ? [type, args] : [type], transferables);
}
async function listener(event: MessageEvent) {
if (terminated) {
return;
}
const {data} = event;
if (!isMessageData(data)) {
return;
}
switch (data[0]) {
case TERMINATE: {
terminate();
break;
}
case CALL: {
const stackFrame = new StackFrame();
const [id, property, args] = data[1];
const func = activeApi.get(property);
try {
if (func == null) {
throw new Error(
`No '${property}' method is exposed on this endpoint`,
);
}
const [encoded, transferables] = encoder.encode(
await func(...(encoder.decode(args, [stackFrame]) as any[])),
);
send(RESULT, [id, undefined, encoded], transferables);
} catch (error) {
const {name, message, stack} = error as Error;
send(RESULT, [id, {name, message, stack}]);
throw error;
} finally {
stackFrame.release();
}
break;
}
case RESULT: {
const [callId, error, result] = data[1];
const resolver = callIdsToResolver.get(callId);
if (resolver == null) {
throw new MissingResolverError({
callId,
error,
result,
});
}
resolver(...data[1]);
callIdsToResolver.delete(callId);
break;
}
case RELEASE: {
const [id] = data[1];
encoder.release(id);
break;
}
case FUNCTION_RESULT: {
const [callId, error, result] = data[1];
const resolver = callIdsToResolver.get(callId);
if (resolver == null) {
throw new MissingResolverError({
callId,
error,
result,
});
}
resolver(...data[1]);
callIdsToResolver.delete(callId);
break;
}
case FUNCTION_APPLY: {
const [callId, funcId, args] = data[1];
try {
const result = await encoder.call(funcId, args);
const [encoded, transferables] = encoder.encode(result);
send(FUNCTION_RESULT, [callId, undefined, encoded], transferables);
} catch (error) {
const {name, message, stack} = error as Error;
send(FUNCTION_RESULT, [callId, {name, message, stack}]);
throw error;
}
break;
}
}
}
function handlerForCall(property: string | number | symbol) {
return (...args: any[]) => {
if (terminated) {
return Promise.reject(
new Error(
'You attempted to call a function on a terminated web worker.',
),
);
}
if (typeof property !== 'string' && typeof property !== 'number') {
return Promise.reject(
new Error(
`Can’t call a symbol method on a remote endpoint: ${property.toString()}`,
),
);
}
const id = uuid();
const done = waitForResult(id);
const [encoded, transferables] = encoder.encode(args);
send(CALL, [id, property, encoded], transferables);
return done;
};
}
function waitForResult(id: string, retainedBy?: Iterable<Retainer>) {
return new Promise<any>((resolve, reject) => {
callIdsToResolver.set(id, (_, errorResult, value) => {
if (errorResult == null) {
resolve(value && encoder.decode(value, retainedBy));
} else {
const error = new Error();
Object.assign(error, errorResult);
reject(error);
}
});
});
}
function terminate() {
terminated = true;
activeApi.clear();
callIdsToResolver.clear();
encoder.terminate?.();
messenger.removeEventListener('message', listener);
}
}
function defaultUuid() {
return `${uuidSegment()}-${uuidSegment()}-${uuidSegment()}-${uuidSegment()}`;
}
function uuidSegment() {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
}
function createCallable<T>(
handlerForCall: (
property: string | number | symbol,
) => AnyFunction | undefined,
callable?: (keyof T)[],
): RemoteCallable<T> {
let call: any;
if (callable == null) {
if (typeof Proxy !== 'function') {
throw new Error(
`You must pass an array of callable methods in environments without Proxies.`,
);
}
const cache = new Map<string | number | symbol, AnyFunction | undefined>();
call = new Proxy(
{},
{
get(_target, property) {
if (cache.has(property)) {
return cache.get(property);
}
const handler = handlerForCall(property);
cache.set(property, handler);
return handler;
},
},
);
} else {
call = {};
for (const method of callable) {
Object.defineProperty(call, method, {
value: handlerForCall(method),
writable: false,
configurable: true,
enumerable: true,
});
}
}
return call;
}
function isMessageData(value: unknown): value is MessageData {
return (
Array.isArray(value) &&
typeof value[0] === 'number' &&
(value[1] == null || Array.isArray(value[1]))
);
}
export {createEndpoint} from './endpoint';
export type {Endpoint, CreateEndpointOptions} from './endpoint';
export {createBasicEncoder} from './encoding';
export {
fromMessagePort,
fromWebWorker,
fromIframe,
fromInsideIframe,
} from './adaptors';
export {
retain,
release,
StackFrame,
isBasicObject,
isMemoryManageable,
RELEASE_METHOD,
RETAIN_METHOD,
RETAINED_BY,
} from './memory';
export type {Retainer, MemoryManageable} from './memory';
export type {
EncodingStrategy,
EncodingStrategyApi,
RemoteCallable,
SafeRpcArgument,
MessageEndpoint,
MaybePromise,
} from './types';
import {RETAINED_BY, RETAIN_METHOD, RELEASE_METHOD} from './types';
import type {Retainer, MemoryManageable} from './types';
export {RETAINED_BY, RETAIN_METHOD, RELEASE_METHOD};
export type {Retainer, MemoryManageable};
export class StackFrame {
private readonly memoryManaged = new Set<MemoryManageable>();
add(memoryManageable: MemoryManageable) {
this.memoryManaged.add(memoryManageable);
memoryManageable[RETAINED_BY].add(this);
memoryManageable[RETAIN_METHOD]();
}
release() {
for (const memoryManaged of this.memoryManaged) {
memoryManaged[RETAINED_BY].delete(this);
memoryManaged[RELEASE_METHOD]();
}
this.memoryManaged.clear();
}
}
export function isMemoryManageable(value: unknown): value is MemoryManageable {
return Boolean(
value && (value as any)[RETAIN_METHOD] && (value as any)[RELEASE_METHOD],
);
}
export function retain(value: any, {deep = true} = {}): boolean {
return retainInternal(value, deep, new Map());
}
function retainInternal(
value: unknown,
deep: boolean,
seen: Map<any, boolean>,
): boolean {
const seenValue = seen.get(value);
if (seenValue != null) return seenValue;
const canRetain = isMemoryManageable(value);
if (canRetain) {
value[RETAIN_METHOD]();
}
seen.set(value, canRetain);
if (deep) {
if (Array.isArray(value)) {
const nestedCanRetain = value.reduce(
(canRetain, item) => retainInternal(item, deep, seen) || canRetain,
canRetain,
);
seen.set(value, nestedCanRetain);
return nestedCanRetain;
}
if (isBasicObject(value)) {
const nestedCanRetain = Object.keys(value).reduce<boolean>(
(canRetain, key) =>
retainInternal((value as any)[key], deep, seen) || canRetain,
canRetain,
);
seen.set(value, nestedCanRetain);
return nestedCanRetain;
}
}
seen.set(value, canRetain);
return canRetain;
}
export function release(value: any, {deep = true} = {}): boolean {
return releaseInternal(value, deep, new Map());
}
export function releaseInternal(
value: any,
deep: boolean,
seen: Map<any, boolean>,
): boolean {
const seenValue = seen.get(value);
if (seenValue != null) return seenValue;
const canRelease = isMemoryManageable(value);
if (canRelease) {
value[RELEASE_METHOD]();
}
seen.set(value, canRelease);
if (deep) {
if (Array.isArray(value)) {
const nestedCanRelease = value.reduce(
(canRelease, item) => releaseInternal(item, deep, seen) || canRelease,
canRelease,
);
seen.set(value, nestedCanRelease);
return nestedCanRelease;
}
if (isBasicObject(value)) {
const nestedCanRelease = Object.keys(value).reduce<boolean>(
(canRelease, key) =>
releaseInternal((value as any)[key], deep, seen) || canRelease,
canRelease,
);
seen.set(value, nestedCanRelease);
return nestedCanRelease;
}
}
return canRelease;
}
export function isBasicObject(value: unknown): value is object {
if (value == null || typeof value !== 'object') return false;
const prototype = Object.getPrototypeOf(value);
return prototype == null || prototype === Object.prototype;
}
class MessagePortPolyfill implements MessagePort {
onmessageerror: EventListener | null = null;
otherPort!: MessagePortPolyfill;
private listeners = new Set<EventListener>();
// MessagePort does not send messages unless it is started via start() or attaching .onmessage
// https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/start
private started = false;
private _onmessage: EventListener | null = null;
// If the port is not yet started, messages will be queued for sending.
private eventQueue: Event[] = [];
get onmessage() {
return this._onmessage;
}
set onmessage(listener: EventListener | null) {
// setting onmessage will start the port, even if the listener is null.
this._onmessage = listener;
this.start();
}
dispatchEvent(event: Event) {
if (!this.started) {
this.eventQueue.push(event);
return true;
}
if (this._onmessage) {
this._onmessage(event);
}
for (const listener of this.listeners) {
listener(event);
}
return true;
}
postMessage(message: any) {
if (!this.otherPort) {
return;
}
this.otherPort.dispatchEvent({data: message} as any);
}
addEventListener(type: string, listener: EventListener) {
if (type !== 'message') {
return;
}
this.listeners.add(listener);
}
removeEventListener(type: string, listener: EventListener) {
if (type !== 'message') {
return;
}
this.listeners.delete(listener);
}
start() {
this.started = true;
while (this.eventQueue.length > 0) {
const event = this.eventQueue.shift();
if (event) {
this.dispatchEvent(event);
}
}
}
close() {
this.started = false;
}
}
class MessageChannelPolyfill implements MessageChannel {
readonly port1: MessagePortPolyfill;
readonly port2: MessagePortPolyfill;
constructor() {
this.port1 = new MessagePortPolyfill();
this.port2 = new MessagePortPolyfill();
this.port1.otherPort = this.port2;
this.port2.otherPort = this.port1;
}
}
export {MessageChannelPolyfill as MessageChannel};
export interface MessageEndpoint {
postMessage(message: any, transferables?: Transferable[]): void;
addEventListener(
event: 'message',
listener: (event: MessageEvent) => void,
): void;
removeEventListener(
event: 'message',
listener: (event: MessageEvent) => void,
): void;
terminate?(): void;
}
export type RemoteCallable<T> = {[K in keyof T]: RemoteCallableField<T[K]>};
type RemoteCallableField<T> = T extends (
...args: infer Args
) => infer TypeReturned
? (...args: Args) => AlwaysAsync<TypeReturned>
: never;
export type MaybePromise<T> = T extends Promise<any> ? T : T | Promise<T>;
type AlwaysAsync<T> = T extends Promise<any>
? T
: T extends infer U | Promise<infer U>
? Promise<U>
: T extends (...args: infer Args) => infer TypeReturned
? (...args: Args) => AlwaysAsync<TypeReturned>
: T extends (infer ArrayElement)[]
? AlwaysAsync<ArrayElement>[]
: T extends ReadonlyArray<infer ArrayElement>
? ReadonlyArray<AlwaysAsync<ArrayElement>>
: T extends object
? {[K in keyof T]: AlwaysAsync<T[K]>}
: T;
export type SafeRpcArgument<T> = T extends (
...args: infer Args
) => infer TypeReturned
? TypeReturned extends Promise<any>
? (...args: Args) => TypeReturned
: (...args: Args) => TypeReturned | Promise<TypeReturned>
: T extends (infer ArrayElement)[]
? SafeRpcArgument<ArrayElement>[]
: T extends ReadonlyArray<infer ArrayElement>
? ReadonlyArray<SafeRpcArgument<ArrayElement>>
: T extends object
? {[K in keyof T]: SafeRpcArgument<T[K]>}
: T;
export const RETAIN_METHOD = Symbol.for('RemoteUi::Retain');
export const RELEASE_METHOD = Symbol.for('RemoteUi::Release');
export const RETAINED_BY = Symbol.for('RemoteUi::RetainedBy');
export interface Retainer {
add(manageable: MemoryManageable): void;
}
export interface MemoryManageable {
readonly [RETAINED_BY]: Set<Retainer>;
[RETAIN_METHOD](): void;
[RELEASE_METHOD](): void;
}
export interface EncodingStrategy {
encode(value: unknown): [any, Transferable[]?];
decode(value: unknown, retainedBy?: Iterable<Retainer>): unknown;
call(id: string, args: any[]): Promise<any>;
release(id: string): void;
terminate?(): void;
}
export interface EncodingStrategyApi {
uuid(): string;
release(id: string): void;
call(id: string, args: any[], retainedBy?: Iterable<Retainer>): Promise<any>;
}
import {createPackage, Runtime} from '@shopify/loom';
import {defaultProjectPlugin} from '../../config/loom';
export default createPackage((pkg) => {
pkg.runtimes(Runtime.Node, Runtime.Browser);
pkg.entry({root: './src/index.ts'});
pkg.use(defaultProjectPlugin());
});
{
"name": "@remote-ui/types",
"version": "1.1.3",
"publishConfig": {
"access": "public",
"@remote-ui:registry": "https://registry.npmjs.org"
},
"license": "MIT",
"main": "index.js",
"module": "index.mjs",
"esnext": "index.esnext",
"types": "./build/ts/index.d.ts",
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
}
},
"sideEffects": false
}
export type RemoteComponentType<
Type extends string,
Props = Record<string, never>,
AllowedChildren extends RemoteComponentType<string, any> | boolean = true,
> =
// If we don't include the object part, this type gets "erased" to just
// be the string type, which means the props/ children can’t be extracted
// from the object later.
Type & {
readonly type?: Type;
readonly props?: Props;
readonly children?: AllowedChildren;
};
export type IdentifierForRemoteComponent<T> = T extends RemoteComponentType<
infer Type,
any,
any
>
? Type
: never;
export type PropsForRemoteComponent<T> = T extends RemoteComponentType<
string,
infer Props,
any
>
? Props
: never;
export type AllowedChildrenForRemoteComponent<T> =
T extends RemoteComponentType<string, any, infer Children>
? Children extends true
? RemoteComponentType<string, any, any>
: Children extends false
? never
: Children
: never;
{
"name": "@shopify/ui-extensions-react",
"version": "2025.7.4",
"description": "React bindings for @shopify/ui-extensions",
"publishConfig": {
"access": "public",
"@shopify:registry": "https://registry.npmjs.org"
},
"sideEffects": false,
"license": "MIT",
"main": "index.js",
"module": "index.mjs",
"esnext": "index.esnext",
"types": "./build/ts/index.d.ts",
"typesVersions": {
"*": {
"admin": [
"./build/ts/surfaces/admin.d.ts"
],
"checkout": [
"./build/ts/surfaces/checkout.d.ts"
],
"customer-account": [
"./build/ts/surfaces/customer-account.d.ts"
],
"point-of-sale": [
"./build/ts/surfaces/point-of-sale.d.ts"
]
}
},
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
},
"./admin": {
"types": "./build/ts/surfaces/admin.d.ts",
"esnext": "./build/esnext/surfaces/admin.esnext",
"import": "./build/esm/surfaces/admin.mjs",
"require": "./build/cjs/surfaces/admin.js"
},
"./checkout": {
"types": "./build/ts/surfaces/checkout.d.ts",
"esnext": "./build/esnext/surfaces/checkout.esnext",
"import": "./build/esm/surfaces/checkout.mjs",
"require": "./build/cjs/surfaces/checkout.js"
},
"./customer-account": {
"types": "./build/ts/surfaces/customer-account.d.ts",
"esnext": "./build/esnext/surfaces/customer-account.esnext",
"import": "./build/esm/surfaces/customer-account.mjs",
"require": "./build/cjs/surfaces/customer-account.js"
},
"./point-of-sale": {
"types": "./build/ts/surfaces/point-of-sale.d.ts",
"esnext": "./build/esnext/surfaces/point-of-sale.esnext",
"import": "./build/esm/surfaces/point-of-sale.mjs",
"require": "./build/cjs/surfaces/point-of-sale.js"
}
},
"dependencies": {
"@remote-ui/async-subscription": "^2.1.16",
"@remote-ui/react": "^5.0.6",
"@types/react": ">=18.2.67"
},
"peerDependencies": {
"@shopify/ui-extensions": "2025.7.4",
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"@shopify/ui-extensions": {
"optional": false
},
"react": {
"optional": false
}
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@quilted/react-testing": "^0.5.31",
"@shopify/ui-extensions": "2025.7.4",
"react": "^18.0.0",
"react-reconciler": "0.29.0",
"react-test-renderer": "^18.2.0"
},
"repository": {
"type": "git",
"url": "https://github.com/Shopify/ui-extensions.git",
"directory": "packages/ui-extensions"
},
"files": [
"build",
"src",
"index.*",
"checkout.*",
"README.md"
],
"eslintConfig": {
"rules": {
"@shopify/strict-component-boundaries": "off",
"react/react-in-jsx-scope": "off"
}
}
}
{
"name": "@shopify/ui-extensions",
"version": "2025.10.15",
"scripts": {
"docs:admin": "node ./docs/surfaces/admin/build-docs.mjs",
"docs:checkout": "bash ./docs/surfaces/checkout/build-docs.sh",
"docs:point-of-sale": "node ./docs/surfaces/point-of-sale/build-docs.mjs",
"docs:customer-account": "node ./docs/surfaces/customer-account/build-docs.mjs"
},
"main": "index.js",
"module": "index.mjs",
"esnext": "index.esnext",
"types": "./build/ts/index.d.ts",
"typesVersions": {
"*": {
"admin": [
"./build/ts/surfaces/admin.d.ts"
],
"admin.*": [
"./build/ts/surfaces/admin/targets/admin.*"
],
"admin/components/*": [
"./build/ts/surfaces/admin/components/*.d.ts"
],
"checkout": [
"./build/ts/surfaces/checkout.d.ts"
],
"checkout/preact": [
"./build/ts/surfaces/checkout/preact/index.d.ts"
],
"purchase.*": [
"./build/ts/surfaces/checkout/targets/purchase.*"
],
"customer-account": [
"./build/ts/surfaces/customer-account.d.ts"
],
"customer-account.*": [
"./build/ts/surfaces/customer-account/targets/customer-account.*.d.ts"
],
"customer-account/preact": [
"./build/ts/surfaces/customer-account/preact/index.d.ts"
],
"point-of-sale": [
"./build/ts/surfaces/point-of-sale.d.ts"
],
"pos.*": [
"./build/ts/surfaces/point-of-sale/targets/pos.*.d.ts"
]
}
},
"exports": {
".": {
"types": "./build/ts/index.d.ts",
"esnext": "./index.esnext",
"import": "./index.mjs",
"require": "./index.js"
},
"./admin": {
"types": "./build/ts/surfaces/admin.d.ts",
"esnext": "./build/esnext/surfaces/admin.esnext",
"import": "./build/esm/surfaces/admin.mjs",
"require": "./build/cjs/surfaces/admin.js"
},
"./admin.*": {
"default": "./build/ts/surfaces/admin/targets/admin.*.d.ts"
},
"./admin/components/*": {
"default": "./build/ts/surfaces/admin/components/*.d.ts"
},
"./checkout": {
"types": "./build/ts/surfaces/checkout.d.ts",
"esnext": "./build/esnext/surfaces/checkout.esnext",
"import": "./build/esm/surfaces/checkout.mjs",
"require": "./build/cjs/surfaces/checkout.js"
},
"./checkout/preact": {
"types": "./build/ts/surfaces/checkout/preact/index.d.ts",
"esnext": "./build/esnext/surfaces/checkout/preact/index.esnext",
"import": "./build/esm/surfaces/checkout/preact/index.mjs",
"require": "./build/cjs/surfaces/checkout/preact/index.js"
},
"./purchase.*": {
"default": "./build/ts/surfaces/checkout/targets/purchase.*.d.ts"
},
"./customer-account": {
"types": "./build/ts/surfaces/customer-account.d.ts",
"esnext": "./build/esnext/surfaces/customer-account.esnext",
"import": "./build/esm/surfaces/customer-account.mjs",
"require": "./build/cjs/surfaces/customer-account.js"
},
"./customer-account.*": {
"default": "./build/ts/surfaces/customer-account/targets/customer-account.*.d.ts"
},
"./customer-account/preact": {
"types": "./build/ts/surfaces/customer-account/preact/index.d.ts",
"esnext": "./build/esnext/surfaces/customer-account/preact/index.esnext",
"import": "./build/esm/surfaces/customer-account/preact/index.mjs",
"require": "./build/cjs/surfaces/customer-account/preact/index.js"
},
"./point-of-sale": {
"types": "./build/ts/surfaces/point-of-sale.d.ts",
"esnext": "./build/esnext/surfaces/point-of-sale.esnext",
"import": "./build/esm/surfaces/point-of-sale.mjs",
"require": "./build/cjs/surfaces/point-of-sale.js"
},
"./pos.*": {
"default": "./build/ts/surfaces/point-of-sale/targets/pos.*.d.ts"
},
"./point-of-sale/preact": {
"types": "./build/ts/surfaces/point-of-sale/preact.d.ts",
"esnext": "./build/esnext/surfaces/point-of-sale/preact.esnext",
"import": "./build/esm/surfaces/point-of-sale/preact.mjs",
"require": "./build/cjs/surfaces/point-of-sale/preact.js"
},
"./preact": {
"types": "./build/ts/preact.d.ts",
"esnext": "./build/esnext/preact.esnext",
"import": "./build/esm/preact.mjs",
"require": "./build/cjs/preact.js"
}
},
"license": "MIT",
"sideEffects": [
"./build/esnext/preact.esnext",
"./build/esm/preact.mjs",
"./build/cjs/preact.js"
],
"devDependencies": {
"@remote-ui/async-subscription": "^2.1.16",
"@shopify/generate-docs": "1.2.2",
"@quilted/react-testing": "^0.6.11",
"typescript": "^4.9.0",
"@faker-js/faker": "^8.4.1",
"preact": "^10.10.x",
"@preact/signals": "^2.3.x"
},
"peerDependencies": {
"preact": "*",
"@preact/signals": "*"
},
"peerDependenciesMeta": {
"preact": {
"optional": true
},
"@preact/signals": {
"optional": true
}
},
"publishConfig": {
"access": "public",
"@shopify:registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/Shopify/ui-extensions.git",
"directory": "packages/ui-extensions"
},
"files": [
"build",
"src",
"index.*",
"README.md"
],
"eslintConfig": {
"rules": {
"@shopify/strict-component-boundaries": "off"
}
},
"dependencies": {
"ts-morph": "^25.0.1"
}
}
/**
* Customize the displayed name of a useState, useReducer or useRef hook
* in the devtools panel.
*
* @param value Wrapped native hook.
* @param name Custom name
*/
export function addHookName<T>(value: T, name: string): T;
Related skills
How it compares
Use this over generic React or Polaris skills when checkout extensions must target Shopify's versioned Storefront GraphQL and Admin APIs rather than standalone component libraries.
FAQ
What does shopify-polaris-checkout-extensions do?
admin name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 storefront-graphql name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion
When should I invoke shopify-polaris-checkout-extensions?
admin name unstable name 2026-07 releaseCandidate true name 2026-04 latestVersion true name 2026-01 name 2025-10 name 2025-07 storefront-graphql name unstable name 2026-07 releaseCandidate true name 2
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is Shopify Polaris Checkout Extensions safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.