UNPKG

@tapcart/app-studio-utils

Version:

A utility library for Tapcart's App Studio

209 lines 10.3 kB
"use strict"; "use client"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.useAppStudioComponents = exports.AppStudioComponentsProvider = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = __importStar(require("react")); const elements_1 = require("../elements"); // Safe useTransition hook that falls back for older React versions const useSafeTransition = () => { // Check if useTransition exists at module level const hasUseTransition = "useTransition" in react_1.default && typeof react_1.default.useTransition === "function"; if (hasUseTransition) { // eslint-disable-next-line react-hooks/rules-of-hooks return react_1.default.useTransition(); } // Fallback for older React versions return [false, (fn) => fn()]; }; const appStudioComponentImportRegex = /import\s+{\s*([^}]+)\s*}\s+from\s+["']@tapcart\/app-studio-components["']/; const AppStudioComponentsContext = react_1.default.createContext({ appStudioComponents: {}, setComponentScope: () => { }, isReady: false, }); 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] = (0, react_1.useState)(null); const [scopeOverrides, setScopeOverrides] = (0, react_1.useState)({}); const [isPending, startTransition] = useSafeTransition(); const scopeHasChanges = (0, react_1.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); }, []); (0, react_1.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 = (0, react_1.useCallback)((component, componentCache) => { // Components can reference each other. When raw `code` is present, scan // it for ES module imports. When it has been stripped (production RSC // path), scan the transpiled CJS output instead: Sucrase binds the // require result to a local variable and accesses named exports as // properties, so we find the binding name then collect every property // access on it. let resolvedDependencies; if (component.code) { const importMatch = component.code.match(appStudioComponentImportRegex); if (importMatch === null || importMatch === void 0 ? void 0 : importMatch[1]) { resolvedDependencies = importMatch[1] .split(",") .map((s) => s.trim()) .filter(Boolean); } } else { const requireMatch = component.transpiledCode.match(/var\s+(\w+)\s*=\s*require\(["']@tapcart\/app-studio-components["']\)/); if (requireMatch === null || requireMatch === void 0 ? void 0 : requireMatch[1]) { const varName = requireMatch[1]; const usageRegex = new RegExp(`\\b${varName}\\.(\\w+)`, "g"); const deps = new Set(); let m; while ((m = usageRegex.exec(component.transpiledCode)) !== null) { deps.add(m[1]); } if (deps.size > 0) resolvedDependencies = Array.from(deps); } } if (resolvedDependencies === null || resolvedDependencies === void 0 ? void 0 : resolvedDependencies.length) { // For each dependency, create that component and store it in the cache for (const dependency of resolvedDependencies) { 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 = (0, elements_1.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) => ((0, jsx_runtime_1.jsx)(AppStudioComponent, Object.assign({}, scopeOverrides, props))); Component.displayName = "AppStudioComponent"; return Component; }, // componentInputs MUST be a dependency: instantiateComponent reads it // when resolving nested @tapcart/app-studio-components imports. Omitting // it captured a stale (often empty) componentInputs, so on first paint // after the components fetch resolved, dependency lookups (e.g. a block // importing ProductCard) failed with "not found in componentInputs" and // the component rendered `undefined`, crashing its ErrorBoundary. [scopeOverrides, componentInputs]); // Full output of instantiated components const appStudioComponents = (0, react_1.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; }, {}); // Use Proxy to return () => null for components that exist but shouldn't render due to missing scope overrides return new Proxy(components, { get(target, prop) { // If useActions is not present in scope overrides, return null for any component if (!scopeOverrides.useActions) { return () => null; } return target[prop]; }, }); }, [scopeOverrides, componentInputs]); return ((0, jsx_runtime_1.jsx)(AppStudioComponentsContext.Provider, { value: { appStudioComponents, setComponentScope, isReady: !isPending, }, children: children })); }; exports.AppStudioComponentsProvider = AppStudioComponentsProvider; const useAppStudioComponents = () => { const context = react_1.default.useContext(AppStudioComponentsContext); if (context === undefined) { throw new Error("useAppStudioComponents must be used within a AppStudioComponentsProvider"); } return context; }; exports.useAppStudioComponents = useAppStudioComponents; //# sourceMappingURL=index.js.map