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) {
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;
}
My approach to the problems was to use
ServiceContainer
To minimize the work of programmers:
// apps/container-experiment/config/*
{
"components": {
"Demo/Banner": "@local/ui/Banner",
"Demo/ServerComponent": [
"@local/ui/server/ServerComponent",
{
"isServer": true
}
],
"Demo/ClientComponent": [
"@local/ui/client/ClientComponent",
{
"isLazy": true
}
]
},
"services": {},
"parameters": {
"searchEngine": "algolia",
"isUserAccountAvailable": false,
}
}
// apps/container-experiment/bin/generate-container.sh
#!/bin/bash
PREV_DIR=`pwd`
__DIR__=`dirname ${BASH_SOURCE[0]}`
LOCAL_ENV_FILE=$__DIR__/../.env.local
if [ -f $LOCAL_ENV_FILE ]; then
source $LOCAL_ENV_FILE
fi
if [ -z "$DOMAIN" ]; then
DOMAIN=default
fi
cd $__DIR__/..
echo "Generating containers for $DOMAIN"
yarn container-generator \
--confDir ./config/$DOMAIN/ \
--verbose \
--serverFile=./src/container/serverContainer.ts \
--clientFile=./src/container/clientContainer.ts
cd $PREV_DIR
// apps/container-experiment/package.json
{
"scripts": {
"dev": "yarn generate:container && next dev",
"build": "yarn generate:container && next build",
"generate:container": "bash bin/generate-container.sh"
},
"dependencies": {
"@almacareer/cyborg.container": "workspace:^"
}
}
// libs/container/src/containerGenerator/index.ts
import commandLineArgs from 'command-line-args';
import chalk from 'chalk';
import { writeFile } from 'node:fs/promises';
import { generateClientContainerCode, generateServerContainerCode } from './generator/generateContainerCode';
import parseConfiguration from './parser/parseConfiguration';
import composeConfiguration from './composer/composeConfiguration';
import findConfigurationFiles from './composer/findConfigurationFiles';
const options = commandLineArgs([
{ name: 'confDir', alias: 'c', type: String, defaultValue: './config' },
{ name: 'serverFile', type: String, defaultValue: 'serverContainer.ts' },
{ name: 'clientFile', type: String, defaultValue: 'clientContainer.ts' },
{ name: 'localConfig', alias: 'l', type: String, defaultValue: './local.services.json' },
{ name: 'verbose', alias: 'v', type: Boolean },
]);
const { confDir, serverFile, clientFile, localConfig } = options;
const files = await findConfigurationFiles(confDir, localConfig);
const configuration = composeConfiguration(files);
const parsedConfiguration = parseConfiguration(configuration.services);
const serverCode = generateServerContainerCode(parsedConfiguration);
const clientCode = generateClientContainerCode(parsedConfiguration);
await writeFile(serverFile, serverCode);
await writeFile(clientFile, clientCode);
// libs/container/src/container/application/types.ts
export interface MutableServiceContainer {
setService: <T>(serviceName: string, service: T) => MutableServiceContainer;
}
export interface ServiceLocator {
getService: <T>(serviceName: string) => T | undefined;
}
interface DependencyInjectionContainer extends ServiceLocator, MutableServiceContainer {}
export interface DependencyInjectionContainerProxy extends DependencyInjectionContainer {
wrapContainer: (container: DependencyInjectionContainer) => DependencyInjectionContainerProxy;
}
export default DependencyInjectionContainer;
✏️ TODO:
set/getComponent & set/getParameter methods
// libs/container/src/container/infrastructure/react/client/ServiceContainerContext.tsx
import React, { createContext, PropsWithChildren, useContext } from 'react';
import createContainer from '@local/container/application/createContainer';
import DependencyInjectionContainer from '@local/container/application/types';
interface ServiceContainerContextType {
container: DependencyInjectionContainer;
}
const defaultValue: ServiceContainerContextType = {
container: createContainer(),
};
const ServiceContainerContext = createContext(defaultValue);
export function ServiceContainerProvider({
container,
children,
}: PropsWithChildren<{ container: DependencyInjectionContainer }>) {
return (
<ServiceContainerContext.Provider value={{ container }}>
{children}
</ServiceContainerContext.Provider>
);
}
export default ServiceContainerContext;
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;
App with container
Library
App with real container
Library
Container(proxy)
Generated Server Container
// apps/container-experiment/src/container/serverContainer.ts
import 'server-only';
import DependencyInjectionContainer from '@almacareer/cyborg.container/container/application/types';
import containerProxy from '@almacareer/cyborg.container/container/infrastructure/react/server';
export function createContainer(): DependencyInjectionContainer {
// ...
}
const container = containerProxy.wrapContainer(createContainer());
export default container;
Generated Client Container
import DependencyInjectionContainer from '@almacareer/cyborg.container/container/application/types';
import DemoBannerc331b7bc7ac84527bf8f4f9d2ae1d20e from '@local/ui/Banner';
export function createContainer(): DependencyInjectionContainer {
const services: Record<string, any> = {
'Demo/Banner': DemoBannerc331b7bc7ac84527bf8f4f9d2ae1d20e,
};
const lazyLoadedServices: Record<string, boolean> = {};
const container: DependencyInjectionContainer = {
setService(serviceName, service) {
services[serviceName] = service;
return this;
},
getService(serviceName) {
switch (serviceName) {
// ...
default:
return services[serviceName];
}
},
};
return container;
}
const container = createContainer();
export default container;
Generated Client Container
import DependencyInjectionContainer from '@almacareer/cyborg.container/container/application/types';
import dynamic from 'next/dynamic';
import DemoBannerc331b7bc7ac84527bf8f4f9d2ae1d20e from '@local/ui/Banner';
export function createContainer(): DependencyInjectionContainer {
//...
const lazyLoadedServices: Record<string, boolean> = {};
const container: DependencyInjectionContainer = {
setService(serviceName, service) { /*...*/ },
getService(serviceName) {
switch (serviceName) {
case 'Demo/ClientComponent':
if (!lazyLoadedServices[serviceName]) {
const service = dynamic(() => import('@local/ui/client/ClientComponent'));
services[serviceName] = service;
lazyLoadedServices[serviceName] = true;
}
return services[serviceName];
default:
return services[serviceName];
}
},
};
return container;
}
const container = createContainer();
export default container;
// apps/container-experiment/src/app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css" />
</head>
<body>
<ContainerProvider>
<Navigation />
<main className="px-2 pt-2">{children}</main>
</ContainerProvider>
</body>
</html>
);
}
// apps/container-experiment/src/container/ui/ContainerProvider.tsx
'use client';
import { PropsWithChildren } from 'react';
import {
ServiceContainerProvider
} from '@almacareer/cyborg.container/container/infrastructure/react/client/ServiceContainerContext';
import container from '@local/container/clientContainer';
export default function ContainerProvider({ children }: PropsWithChildren) {
return (
<ServiceContainerProvider container={container}>
{children}
</ServiceContainerProvider>
);
}
// apps/container-experiment/src/../SomeComponent.tsx
import container from '@local/container/serverContainer';
import { ComponentType } from 'react';
export default function SomeComponent() {
const ServerComponent = container.getService<ComponentType>('Demo/ServerComponent');
const ClientComponent = container.getService<ComponentType>('Demo/ClientComponent');
return (
<>
<div>
<div>
{ServerComponent ? <ServerComponent /> : null}
</div>
<div>
{ClientComponent ? <ClientComponent /> : null}
</div>
</div>
</>
);
}
// libs/search/src/ui/List.tsx
import React, { ComponentType } from 'react';
import container from '@almacareer/cyborg.container/container/infrastructure/react/server';
import Item from './Item';
function NullComponent() {
return null;
}
export default function List() {
const items = ['Programmer', 'Product Manager', 'Delivery Manager', 'CEO', 'UX designer'];
const HotOffers = container.getService<ComponentType>('Search/HotOffers') ?? NullComponent;
return (
<div>
<HotOffers />
<div className="message is-info">
<div className="message-body">
This list comes from package <code>@almacareer/cyborg.search</code> and it is server component.
</div>
</div>
{items.map((item) => {
return <Item key={`item-${item}`} title={item} />;
})}
</div>
);
}
// apps/container-experiment/src/../SomeComponent.tsx
'use client';
import { ComponentType } from 'react';
import {
useService
} from '@almacareer/cyborg.container/container/infrastructure/react/client/ServiceContainerContext';
export default function ClientPage() {
const ClientComponent = useService<ComponentType>('Demo/ClientComponent');
return (
<>
<div>
{ClientComponent ? <ClientComponent /> : null}
</div>
</>
);
}
// libs/search/src/ui/HotOffers.tsx
'use client';
import React from 'react';
import Item from './Item';
import {
useService
} from '@almacareer/cyborg.container/container/infrastructure/react/client/ServiceContainerContext';
export default function HotOffers() {
const items = ['Hot offer 1', 'Hot offer 2'];
const ClientComponent = useService<ComponentType>('Demo/ClientComponent') ?? NullComponent;
return (
<div>
<div className="message is-info">
<div className="message-body">
This Hot Offers list comes from package <code>@almacareer/cyborg.search</code>
and it is client component
</div>
</div>
{items.map((item) => {
return <Item key={`item-${item}`} title={item} bgVariant="danger" />;
})}
<ClientComponent />
</div>
);
}
From | Import | Usage |
---|---|---|
application, server | @local/serverContainer | container.getService |
library, server | @container/server | container.getService |
application, server | @container/Context | useService |
library, client | @container/Context | useService |