Longer, detailed and not finished version
Milan Herda, 07/2024
'use client';
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
import SearchForm from '@local/ui/search/SearchForm';
import useHostname from '@local/router/infrastructure/react/useHostname';
export default function Homepage() {
const hostname = useHostname();
let HeroBanner = DefaultHeroBanner;
if (hostname === 'prace.cz') {
HeroBanner = HeroBannerPrace;
} else if (hostname === 'jobs.cz') {
HeroBanner = HeroBannerJobs;
}
return (
<>
<HeroBanner />
<SearchForm />
<>
</>
);
}
'use client';
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
import SearchForm from '@local/ui/search/SearchForm';
import useHostname from '@local/router/infrastructure/react/useHostname';
export default function Homepage() {
const hostname = useHostname();
let HeroBanner = DefaultHeroBanner;
if (hostname === 'prace.cz') {
HeroBanner = HeroBannerPrace;
} else if (hostname === 'jobs.cz') {
HeroBanner = HeroBannerJobs;
}
return (
<>
<HeroBanner />
<SearchForm />
<>
</>
);
}
'use client';
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
import SearchForm from '@local/ui/search/SearchForm';
import useHostname from '@local/router/infrastructure/react/useHostname';
export default function Homepage() {
const hostname = useHostname();
const HeroBanner = resolveHeroBanner(hostname)
return (
<>
<HeroBanner />
<SearchForm />
<>
</>
);
}
function resolveHeroBanner(hostname: string, componentName: string) {
let HeroBanner = DefaultHeroBanner;
if (hostname === 'prace.cz') {
HeroBanner = HeroBannerPrace;
} else if (hostname === 'jobs.cz') {
HeroBanner = HeroBannerJobs;
}
return HeroBanner;
}
'use client';
import SearchForm from '@local/ui/search/SearchForm';
import useHostname from '@local/router/infrastructure/react/useHostname';
import resolveHeroBanner from '@local/resolver/application/resolveBanner';
export default function Homepage() {
const hostname = useHostname();
const HeroBanner = resolveHeroBanner(hostname)
return (
<>
<HeroBanner />
<SearchForm />
</>
);
}
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
export default function resolveHeroBanner(hostname: string) {
let HeroBanner = DefaultHeroBanner;
if (hostname === 'prace.cz') {
HeroBanner = HeroBannerPrace;
} else if (hostname === 'jobs.cz') {
HeroBanner = HeroBannerJobs;
}
return HeroBanner;
}
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
export default function resolveHeroBanner(hostname: string) {
let HeroBanner = DefaultHeroBanner;
if (hostname === 'prace.cz') {
HeroBanner = HeroBannerPrace;
} else if (hostname === 'jobs.cz') {
HeroBanner = HeroBannerJobs;
}
return HeroBanner;
}
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
const componentsRegistry = {
'prace.cz': HeroBannerPrace,
'jobs.cz': HeroBannerJobs,
};
export default function resolveHeroBanner(hostname: string) {
if (componentsRegistry[hostname]) {
return componentsRegistry[hostname];
}
return DefaultHeroBanner;
}
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
const componentsRegistry = {
'default': DefaultHeroBanner,
'prace.cz': HeroBannerPrace,
'jobs.cz': HeroBannerJobs,
};
export default function resolveHeroBanner(hostname: string) {
if (componentsRegistry[hostname]) {
return componentsRegistry[hostname];
}
return componentsRegistry.default ?? () => null;
}
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
const componentsRegistry = {
'default': DefaultHeroBanner,
'prace.cz': HeroBannerPrace,
'jobs.cz': HeroBannerJobs,
};
export default function resolveComponent(hostname: string, componentName: string) {
if (componentsRegistry[hostname]) {
return componentsRegistry[hostname];
}
return componentsRegistry.default ?? () => null;
}
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
const componentsRegistry = {
'default': {
HeroBanner: DefaultHeroBanner,
},
'prace.cz': {
HeroBanner: HeroBannerPrace,
},
'jobs.cz': {
HeroBanner: HeroBannerJobs,
},
};
export default function resolveComponent(hostname: string, componentName: string) {
if (componentsRegistry[hostname]?.[componentName]) {
return componentsRegistry[hostname][componentName];
}
return componentsRegistry.default[componentName] ?? () => null;
}
import componentsRegistry from '@local/configuration/componentsRegistry';
export default function resolveComponent(hostname: string, componentName: string) {
if (componentsRegistry[hostname]?.[componentName]) {
return componentsRegistry[hostname][componentName];
}
return componentsRegistry.default[componentName] ?? () => null;
}
import componentsRegistry from '@local/configuration/componentsRegistry';
export default function resolveComponent(hostname: string, componentName: string) {
if (componentsRegistry[hostname]?.[componentName]) {
return componentsRegistry[hostname][componentName];
}
return componentsRegistry.default[componentName] ?? () => null;
}
import serviceRegistry from '@local/configuration/serviceRegistry';
export default function resolveService(hostname: string, serviceName: string) {
if (serviceRegistry[hostname]?.[serviceName]) {
return serviceRegistry[hostname][serviceName];
}
return serviceRegistry.default[serviceName] ?? () => null;
}
ServiceRegistry
// src/configuration/serviceRegistry
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
import JobOfferRepository from '@search/jobs/repository/JobOfferRepository';
const serviceRegistry = {
'default': {
HeroBanner: DefaultHeroBanner,
jobOfferRepository: JobOfferRepository,
},
'prace.cz': {
HeroBanner: HeroBannerPrace,
},
'jobs.cz': {
HeroBanner: HeroBannerJobs,
},
};
export default serviceRegistry;
ServiceRegistry => Registry
// src/configuration/serviceRegistry
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
import jobOfferRepository from '@search/jobs/repository/jobOfferRepository';
const serviceRegistry = {
'default': {
HeroBanner: DefaultHeroBanner,
jobOfferRepository: jobOfferRepository,
},
'prace.cz': {
HeroBanner: HeroBannerPrace,
},
'jobs.cz': {
HeroBanner: HeroBannerJobs,
},
};
export default serviceRegistry;
ServiceRegistry => Registry
// src/configuration/serviceRegistry
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
import jobOfferRepository from '@search/jobs/repository/jobOfferRepository';
const registry = {
services: {
'default': {
HeroBanner: DefaultHeroBanner,
jobOfferRepository: jobOfferRepository,
},
'prace.cz': {
HeroBanner: HeroBannerPrace,
},
'jobs.cz': {
HeroBanner: HeroBannerJobs,
},
}
};
export default registry;
ServiceRegistry => Registry
// src/configuration/registry
import HeroBannerPrace from '@prace/ui/HeroBanner';
import HeroBannerJobs from '@jobs/ui/Homepage/HeroBanner';
import DefaultHeroBanner from '@local/ui/hero/HeroBanner';
import jobOfferRepository from '@search/jobs/repository/jobOfferRepository';
const registry = {
services: {
'default': {
HeroBanner: DefaultHeroBanner,
jobOfferRepository: jobOfferRepository,
},
'prace.cz': {
HeroBanner: HeroBannerPrace,
},
'jobs.cz': {
HeroBanner: HeroBannerJobs,
},
},
parameters: {
'default': {
searchEngine: 'job-matching',
},
'prace.cz': {
searchEngine: 'algolia',
},
'jobs.cz': {
searchEngine: 'elastic'
},
},
};
export default registry;
ServiceRegistry => Registry
const registry = {
components: {
'default': {
HeroBanner: DefaultHeroBanner,
},
'prace.cz': {
HeroBanner: HeroBannerPrace,
},
'jobs.cz': {
HeroBanner: HeroBannerJobs,
},
},
services: {
'default': {
jobOfferRepository: jobOfferRepository,
},
},
parameters: {
'default': {
searchEngine: 'job-matching',
},
'prace.cz': {
searchEngine: 'algolia',
},
'jobs.cz': {
searchEngine: 'elastic'
},
},
};
export default registry;
"Resolvers"
import registry from '@local/configuration/registry';
export function resolveComponent(hostname: string, componentName: string) {
const { components } = registry;
if (components[hostname]?.[componentName]) {
return components[hostname][componentName];
}
return components.default?.[componentName] ?? () => null;
}
export function resolveService(hostname: string, serviceName: string) {
const { services } = registry;
if (services[hostname]?.[serviceName]) {
return services[hostname][serviceName];
}
return services.default?.[serviceName];
}
export function getParameter(hostname: string, name: string) {
const { parameters } = registry;
if (parameters[hostname]?.[name]) {
return parameters[hostname][name];
}
return parameters.default?.[name];
}
export function resolveComponent<T>(hostname: string, componentName: string): T {
// ...
}
export function resolveService<T>(hostname: string, serviceName: string): T {
// ...
}
'use client';
import SearchForm from '@local/ui/search/SearchForm';
import useHostname from '@local/router/infrastructure/react/useHostname';
import { ComponentType } from 'react';
import type { HeroBannerProps } from '@local/ui/homepage/HeroBanner';
export default function Homepage() {
const hostname = useHostname();
const HeroBanner = resolveComponent<ComponentType<HeroBannerProps>>(hostname, 'HeroBanner')
return (
<>
<HeroBanner title="The Best Job Offers!" />
<SearchForm />
</>
);
}
// /src/configuration/registry/prace
import HeroBanner from '@prace/ui/HeroBanner';
import jobOfferRepository from '@search/jobs/repository/jobOfferRepository';
const registry = {
components: {
HeroBanner: HeroBanner,
},
services: {
jobOfferRepository: jobOfferRepository,
},
parameters: {
searchEngine: 'algolia',
},
};
export default registry;
// /src/configuration/resolver
export async function resolveComponent(hostname: string, componentName: string) {
const registryModule = await import(`./registry/${hostname}`);
const registry = registryModule.default;
const { components } = registry;
if (components?.[componentName]) {
return components[componentName];
}
return () => null;
}
We can dynamically import only the required one
// /src/configuration/resolver
import registry from '@local/configuration/registry'
export function resolveComponent(componentName: string) {
const { components } = registry;
if (components?.[componentName]) {
return components[componentName];
}
return () => null;
}
Or we can build only one registry during the deployment
and it will be tied to the deployed hostname-instance
The registry is still too large, can we ... maybe ... lazy load some components and services?
// /src/configuration/registry/prace
import HeroBanner from '@prace/ui/HeroBanner';
import jobOfferRepository from '@search/jobs/repository/jobOfferRepository';
const registry = {
components: {
HeroBanner: HeroBanner,
SearchForm: '@local/ui/SearchForm',
},
services: {
jobOfferRepository: jobOfferRepository,
},
parameters: {
searchEngine: 'algolia',
},
};
export default registry;
// /src/configuration/resolver
import { ComponentType } from 'react';
import dynamic from 'next/dynamic'
import registry from '@local/configuration/registry'
function DefaultSkeleton() {
return null;
//return <p>Loading...</p>;
}
export function resolveComponent(
componentName: string,
skeleton: ComponentType = DefaultSkeleton
) {
const { components } = registry;
if (components?.[componentName]) {
if (typeof components[componentName] === 'string') {
return dynamic(() => import(components[componentName]), {
loading: skeleton,
});
}
return components[componentName];
}
return () => null;
}
return dynamic(() => import(components[componentName]), {
loading: skeleton,
});
We can not use only variables in dynamic imports.
We need to help bundler to know what files can be loaded
But they can be placed anywhere...
return dynamic(
() => {
return import(`@local/ui/agreed-place-1/${componentName}`)
.catch(
() => import(`@search/ui/agreed-place-2/${componentName}`)
);
}, {
loading: skeleton,
}
);
We can either restrict programmers and tell them they need to place their components in a few agreed locations
// /src/configuration/resolver
import { ComponentType } from 'react';
import dynamic from 'next/dynamic'
import registry from '@local/configuration/registry'
function DefaultSkeleton() {
return null;
//return <p>Loading...</p>;
}
export function resolveComponent(
componentName: string,
skeleton: ComponentType = DefaultSkeleton
) {
const { components } = registry;
if (componentName === 'SearchForm') {
return dynamic(() => import('@local/ui/SearchForm'), {
loading: skeleton,
});
}
return components[componentName] ?? () => null;
}
To be able to do so, we need some form of
"recipe"
configuration
"recipe"
// /src/configuration/registry/prace
import HeroBanner from '@prace/ui/HeroBanner';
import jobOfferRepository from '@search/jobs/repository/jobOfferRepository';
const registry = {
components: {
HeroBanner: HeroBanner,
SearchForm: '@local/ui/SearchForm',
},
services: {
jobOfferRepository: jobOfferRepository,
},
parameters: {
searchEngine: 'algolia',
},
};
export default registry;
// /src/config/prace/config.json
{
"components": {
"HeroBanner": "@prace/ui/HeroBanner",
"SearchForm": [
"@local/ui/SearchForm",
{
"isLazy": true
}
]
},
"services": {
"jobOfferRepository": "@search/jobs/repository/jobOfferRepository"
},
"parameters": {
"searchEngine": "algolia"
}
}
// /src/config/prace/config.json
{
"components": {
"HeroBanner": [
"@prace/ui/HeroBanner",
{
"isServerSide": true,
}
],
"SearchForm": [
"@local/ui/SearchForm",
{
"isLazy": true
}
]
},
"services": {
"jobOfferRepository": "@search/jobs/repository/jobOfferRepository"
},
"parameters": {
"searchEngine": "algolia"
}
}
// /src/config/prace/config.json
{
"components": {
"HeroBanner": [
"@prace/ui/HeroBanner",
{
"isServerSide": true,
}
],
"SearchForm": [
"@local/ui/SearchForm",
{
"isLazy": true
}
]
},
"services": {
"jobOfferRepository": "@search/jobs/repository/jobOfferRepository"
},
"parameters": {
"searchEngine": "algolia"
}
}
App with register & resolver
Library
App with register
Library
Resolver
// libs/search/src/ui/Listing/Item.tsx
'use client'
import { useContext } from 'react';
import ServiceContainerContext from '@container/infrastructure/react/ServiceContainerContext';
export default function Item() {
const { resolveComponent } = useContext(ServiceContainerContext);
const OfferLabel = resolveComponent('Listing/OfferLabel');
return (
<>
{/*...*/}
<OfferLabel />
{/*...*/}
</>
);
}
// libs/search/src/ui/Listing/Item.tsx
import container from '@container/container/infrastructure/server';
export default function Item() {
const OfferLabel = container.resolveComponent('Listing/OfferLabel');
return (
<>
{/*...*/}
<OfferLabel />
{/*...*/}
</>
);
}
// libs/container/src/container/infrastructure/server.ts
import 'server-only';
import createContainer from '@local/container/application/createContainer';
import DependencyInjectionContainer, {
DependencyInjectionContainerProxy
} from '@local/container/application/types';
export function createProxyContainer(): DependencyInjectionContainerProxy {
let wrappedContainer: DependencyInjectionContainer = createContainer();
return {
setService(serviceName, service) {
wrappedContainer.setService(serviceName, service);
return this;
},
getService(serviceName) {
return wrappedContainer.getService(serviceName);
},
wrapContainer(container) {
wrappedContainer = container;
return this;
},
};
}
const container: DependencyInjectionContainerProxy = createProxyContainer();
export default container;