@tapcart/app-studio-utils
Version:
A utility library for Tapcart's App Studio
209 lines • 10.3 kB
JavaScript
;
"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