
Shopify Polaris Admin Extensions
- 6.9k installs
- 484 repo stars
- Updated July 27, 2026
- shopify/shopify-ai-toolkit
shopify-polaris-admin-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 Admin Extensions by the numbers
- 6,937 all-time installs (skills.sh)
- +282 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #227 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
shopify-polaris-admin-extensions capabilities & compatibility
- Capabilities
- see skill.md documentation
- Use cases
- documentation
What shopify-polaris-admin-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-admin-extensionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.9k |
|---|---|
| repo stars | ★ 484 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | shopify/shopify-ai-toolkit ↗ |
When should developers use shopify-polaris-admin-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-admin-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-admin-extensions.
What you get
Grounded guidance and workflows from SKILL.md for shopify-polaris-admin-extensions.
- admin extension UI code
- GraphQL integration stubs
By the numbers
- Lists six named Admin API versions from unstable through 2025-07
- Marks 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 shopify-polaris-admin-extensions when building embedded admin UI on Shopify rather than generic React component generators.
FAQ
What does shopify-polaris-admin-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-admin-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 Admin 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.