@tapcart/app-studio-utils
Version:
A utility library for Tapcart's App Studio
124 lines • 6.28 kB
JavaScript
"use client";
import { jsx as _jsx } from "react/jsx-runtime";
import React, { useCallback, useEffect, useMemo, useState, useTransition, } from "react";
import { generateElement } from "../elements";
const appStudioComponentImportRegex = /import\s+{\s*([^}]+)\s*}\s+from\s+["']@tapcart\/app-studio-components["']/;
const AppStudioComponentsContext = React.createContext({
appStudioComponents: {},
setComponentScope: () => { },
isReady: false,
});
export const AppStudioComponentsProvider = ({ children, componentInputs, scopeInput, }) => {
// Component scope should be set once and then not updated again unless the page state is changed.
// We expose a function for updating the scope manually if needed. (Ex: Server scope vs client scope and we need client scope here)
const [componentScope, setComponentScope] = useState(null);
const [scopeOverrides, setScopeOverrides] = useState({});
const [isPending, startTransition] = useTransition();
const scopeHasChanges = useCallback((oldScope, newScope) => {
// Remove page state from comparison
// Page state is static except for the search params - search params are delivered through useSearchParams hook
// Do not need to regenerate components if only search params have changed. Also provides better user experience
// Copy objects first to prevent unintended mutations of the original scope objects
const scope1 = Object.assign({}, oldScope);
const scope2 = Object.assign({}, newScope);
delete scope1.pageState;
delete scope2.pageState;
// JSON.stringify will remove undefined values and functions
// Compare the number of keys to determine if the scope has changed
if (Object.keys(scope1).length !== Object.keys(scope2).length) {
return true;
}
return JSON.stringify(scope1) !== JSON.stringify(scope2);
}, []);
useEffect(() => {
const currentScope = componentScope || scopeInput;
if (!currentScope)
return;
let newScope = Object.assign({}, currentScope);
// Remove block specific properties from scope overrides as this is not a block
delete newScope.blockConfig;
delete newScope.blockId;
// TODO: Implement manifest options
newScope.componentConfig = {};
// Check if the scope has changed
if (!scopeHasChanges(scopeOverrides, newScope))
return;
startTransition(() => {
setScopeOverrides(newScope);
});
}, [componentScope, scopeInput]);
// Responsible for instantiating a component and its dependencies
const instantiateComponent = useCallback((component, componentCache) => {
// Components can reference each other. Find app studio component dependencies in component code
const importMatch = component.code.match(appStudioComponentImportRegex);
// If there is an import, there are other custom component dependencies that we need to load
if (importMatch === null || importMatch === void 0 ? void 0 : importMatch.length) {
// Extract the component dependencies from the import statement
const strippedImport = importMatch[0]
.replace(/import {|}|"|;|from|@tapcart\/app-studio-components/g, "")
.trim();
const componentDependencies = strippedImport
.split(",")
.map((dep) => dep.trim());
// For each dependency, create that component and store it in the cache
for (const dependency of componentDependencies) {
if (!dependency || componentCache[dependency])
continue;
// Find dependency in componentInputs
const dependencyComponent = componentInputs[dependency];
if (!dependencyComponent) {
console.warn(`App studio component "${dependency}" not found in componentInputs`);
continue;
}
const dependencyComponentInstance = instantiateComponent(dependencyComponent, componentCache);
if (!dependencyComponentInstance)
continue;
componentCache[dependency] = dependencyComponentInstance;
}
}
// With dependencies loaded, instantiate the component
const AppStudioComponent = generateElement({
code: component.transpiledCode,
scopeOverrides,
appStudioComponents: componentCache,
});
if (!AppStudioComponent) {
console.error(`Failed to instantiate app studio component ${component.key}`);
return null;
}
// Return function that renders the component. Do this so we can inject our scope as well as block related props.
const Component = (props) => (_jsx(AppStudioComponent, Object.assign({}, scopeOverrides, props)));
Component.displayName = "AppStudioComponent";
return Component;
}, [scopeOverrides]);
// Full output of instantiated components
const appStudioComponents = useMemo(() => {
if (isPending)
return {};
let componentCache = {};
// Turn app studio component inputs into a map of component name to component function
const components = Object.keys(componentInputs).reduce((acc, key) => {
if (!componentInputs[key])
return acc;
const component = instantiateComponent(componentInputs[key], componentCache);
if (!component)
return acc;
acc[key] = component;
return acc;
}, {});
return components;
}, [scopeOverrides, componentInputs]);
return (_jsx(AppStudioComponentsContext.Provider, { value: {
appStudioComponents,
setComponentScope,
isReady: !isPending,
}, children: children }));
};
export const useAppStudioComponents = () => {
const context = React.useContext(AppStudioComponentsContext);
if (context === undefined) {
throw new Error("useAppStudioComponents must be used within a AppStudioComponentsProvider");
}
return context;
};
//# sourceMappingURL=index.js.map