feat: connect application input and output boundaries

This commit is contained in:
donghyeon-ka
2026-07-26 13:52:35 +09:00
parent 38ad69236b
commit 2dda17cf19
43 changed files with 697 additions and 215 deletions
+15 -6
View File
@@ -6,27 +6,36 @@ import { createApplication } from "../application/create-application.js";
*
* @template Config
* @template Release
* @template {Parameters<typeof createApplication>[0]} Ports
* @template {Parameters<typeof createApplication>[0]} OutputPorts
* @template Infrastructure
* @param {{
* loadConfig(): Promise<Config>,
* loadRelease(config: Config): Promise<Release>,
* createAdapters(context: {
* config: Config,
* release: Release
* }): Promise<Ports>
* }): Promise<{
* outputPorts: OutputPorts,
* infrastructure: Infrastructure
* }>
* }} factories
* @returns {Promise<Readonly<{
* config: Config,
* release: Release,
* ports: Ports,
* infrastructure: Infrastructure,
* application: ReturnType<typeof createApplication>
* }>>}
*/
export async function createCompositionRoot(factories) {
const config = await factories.loadConfig();
const release = await factories.loadRelease(config);
const ports = await factories.createAdapters({ config, release });
const application = createApplication(ports);
const adapters = await factories.createAdapters({ config, release });
const application = createApplication(adapters.outputPorts);
return Object.freeze({ config, release, ports, application });
return Object.freeze({
config,
release,
infrastructure: adapters.infrastructure,
application,
});
}
+3 -4
View File
@@ -6,22 +6,21 @@ import {
/**
* Applies the persisted public preference before React paints.
*
* @param {import("../application/ports/storage-port.js").StoragePort} storage
* @param {Pick<import("../application/ports/in/application-api.js").ApplicationApi["preferences"], "getColorScheme">} preferences
* @param {{
* documentElement?: HTMLElement,
* matchMedia?: (query: string) => MediaQueryList
* }} [browser]
*/
export function initializeColorScheme(storage, browser = {}) {
export function initializeColorScheme(preferences, browser = {}) {
const documentElement = browser.documentElement ?? document.documentElement;
const matchMedia =
browser.matchMedia ??
(typeof window.matchMedia === "function"
? window.matchMedia.bind(window)
: () => /** @type {MediaQueryList} */ ({ matches: false }));
const stored = storage.read("COLOR_SCHEME");
const preference = normalizeColorSchemePreference(
stored.ok ? stored.value : undefined,
preferences.getColorScheme(),
);
const resolved = resolveColorScheme(
preference,
+3 -17
View File
@@ -1,13 +1,11 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClientProvider } from "@tanstack/react-query";
import { BootErrorShell } from "../presentation/boundaries/boot-error-shell.jsx";
import { AppRouter } from "../presentation/routes/app-router.jsx";
import { createRuntimeComposition } from "./create-runtime-composition.js";
import { initializeColorScheme } from "./initialize-color-scheme.js";
import { BootConfigError } from "./load-runtime-config.js";
import { ReleaseManifestError } from "./load-release-manifest.js";
import { RuntimeApplication } from "./runtime-application.jsx";
import "../presentation/styles/theme.css";
const rootElement = document.getElementById("root");
@@ -21,20 +19,8 @@ const root = createRoot(rootElement);
async function boot() {
try {
const composition = await createRuntimeComposition();
initializeColorScheme(composition.ports.storage);
root.render(
<StrictMode>
<QueryClientProvider client={composition.ports.queryClient}>
<AppRouter
authSession={composition.ports.authSession}
basename={composition.config.build.routerBasePath}
buildId={composition.release.buildId}
storage={composition.ports.storage}
telemetry={composition.ports.telemetry}
/>
</QueryClientProvider>
</StrictMode>,
);
initializeColorScheme(composition.application.preferences);
root.render(<RuntimeApplication composition={composition} />);
} catch (error) {
const safe =
error instanceof BootConfigError || error instanceof ReleaseManifestError
+9 -15
View File
@@ -3,9 +3,7 @@ import {
createExternalAuthSessionAdapter,
createUnavailableSessionAdapter,
} from "../adapters/auth/external-session-adapter.js";
import { createHttpClient } from "../adapters/http/client.js";
import {
createQueryCacheAdapter,
createQueryClient,
} from "../adapters/query-cache/tanstack-query-cache.js";
import { createBrowserStorageAdapter } from "../adapters/storage/browser-storage-adapter.js";
@@ -61,7 +59,6 @@ export async function createRuntimeAdapters(context) {
? createExternalAuthSessionAdapter(externalOwner)
: createUnavailableSessionAdapter();
const queryClient = createQueryClient();
const cache = createQueryCacheAdapter(queryClient);
const storage = createBrowserStorageAdapter({
localStorage: storageOrUndefined(host.localStorage),
sessionStorage: storageOrUndefined(host.sessionStorage),
@@ -71,11 +68,6 @@ export async function createRuntimeAdapters(context) {
endpoint: config.TELEMETRY_ENDPOINT,
fetcher: context.fetcher,
});
const http = createHttpClient({
baseUrl: config.API_BASE_URL,
authSession,
fetcher: context.fetcher,
});
const releaseInfo = Object.freeze({
async getCurrent() {
return structuredClone(context.release);
@@ -83,12 +75,14 @@ export async function createRuntimeAdapters(context) {
});
return Object.freeze({
authSession,
cache,
http,
queryClient,
releaseInfo,
storage,
telemetry,
outputPorts: Object.freeze({
session: authSession,
preferences: storage,
diagnostics: telemetry,
releaseInfo,
}),
infrastructure: Object.freeze({
queryClient,
}),
});
}
+28
View File
@@ -0,0 +1,28 @@
import { StrictMode } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { ApplicationProvider } from "../presentation/providers/application-provider.js";
import { AppRouter } from "../presentation/routes/app-router.jsx";
/**
* Production provider tree. Tests import this component so the validated
* composition is proven against the same provider order used by main.
*
* @param {{
* composition: Awaited<ReturnType<typeof import("./create-runtime-composition.js").createRuntimeComposition>>
* }} props
*/
export function RuntimeApplication({ composition }) {
return (
<StrictMode>
<QueryClientProvider client={composition.infrastructure.queryClient}>
<ApplicationProvider application={composition.application}>
<AppRouter
basename={composition.config.build.routerBasePath}
buildId={composition.release.buildId}
/>
</ApplicationProvider>
</QueryClientProvider>
</StrictMode>
);
}