/** * Describes a file that represents a component definition */ export interface ComponentFile { path: string; moduleName: string; componentName: string; } export interface PackageDefinition { name: string; components: { moduleName: string; componentName: string; }[]; } const isLazyLoadingModule = (componentPath: string) => componentPath.includes(".dynamic"); const removeDynamicModuleNameEnding = (moduleName: string) => moduleName.replace(/\.?dynamic$/i, ""); /** * Generates the contents of the component factory file using a predefined string template. * @param components - the list of component files to include * @returns component factory file contents */ function generateComponentFactory( components: (PackageDefinition | ComponentFile)[], ): string { const componentFiles = components.filter( (component) => (component as ComponentFile).path, ) as ComponentFile[]; const packages = components.filter( (component) => (component as PackageDefinition).components, ) as PackageDefinition[]; const hasLazyModules = componentFiles.find((component) => isLazyLoadingModule(component.path), ); return `/* eslint-disable */ // Do not edit this file, it is auto-generated at build time! // See scripts/generate-component-factory.ts to modify the generation of this file. ${hasLazyModules ? "import dynamic from 'next/dynamic'" : ""} ${packages.map((pkg) => { const list = pkg.components.map((c) => c.moduleName).join(", "); return `import { ${list} } from '${pkg.name}'`; })} ${componentFiles .map((component) => { if (isLazyLoadingModule(component.path)) { const moduleName = removeDynamicModuleNameEnding(component.moduleName); return `const ${moduleName} = { module: () => import('${component.path}'), element: (isEditing?: boolean) => isEditing ? require('${component.path}')?.default : dynamic(${moduleName}.module) }`; } return `import * as ${component.moduleName} from '${component.path}';`; }) .join("\n")} const components = new Map(); ${packages.map((p) => p.components.map( (component) => `components.set('${component.componentName}', ${component.moduleName})`, ), )} ${componentFiles .map( (component) => `components.set('${ isLazyLoadingModule(component.path) ? removeDynamicModuleNameEnding(component.componentName) : component.componentName }', ${ isLazyLoadingModule(component.path) ? removeDynamicModuleNameEnding(component.moduleName) : component.moduleName });`, ) .join("\n")} // Next.js 'dynamic' import and JavaScript 'dynamic' import are different. // Next.js 'dynamic(...)' returns common 'React.ComponentType' while // 'import('...')' returns 'Promise' that will resolve module. // componentModule uses 'import(...)' because primary usage of it to get not only 'React Component' (default export) but all named exports. // See https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports // componentFactory uses 'dynamic(...)' because primary usage of it to render 'React Component' (default export). // See https://nextjs.org/docs/advanced-features/dynamic-import // At the end you will have single preloaded script for each lazy loading module. // Editing mode doesn't work well with dynamic components in nextjs: dynamic components are not displayed without refresh after a rendering is added. // This happens because Sitecore editors simply insert updated HTML generated on server side. This conflicts with nextjs dynamic logic as no HTML gets rendered for dynamic component // So we use require() to obtain dynamic components in editing mode while preserving dynamic logic for non-editing scenarios // As we need to be able to seamlessly work with dynamic components in both editing and normal modes, different componentFactory functions will be passed to app export function componentModule(componentName: string) { const component = components.get(componentName); // check that component is lazy loading module if (!component?.default && component?.module) { // return js dynamic import return component.module(); } return component; } function baseComponentFactory(componentName: string, exportName?: string, isEditing?: boolean) { const DEFAULT_EXPORT_NAME = 'Default'; const component = components.get(componentName); // check that component should be dynamically imported if (component?.element) { // return next.js dynamic import return component.element(isEditing); } if (exportName && exportName !== DEFAULT_EXPORT_NAME) { return component[exportName]; } return component?.Default || component?.default || component; } export function componentFactory(componentName: string, exportName?: string) { return baseComponentFactory(componentName, exportName, false); } export function editingComponentFactory(componentName: string, exportName?: string) { return baseComponentFactory(componentName, exportName, true); } `; } export default generateComponentFactory;