UNPKG

@tapcart/app-studio-utils

Version:

A utility library for Tapcart's App Studio

124 lines 6.28 kB
"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