Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3aaceb4
fix(package): remove unnecessary command from test script
marcuscastelo Dec 6, 2025
ba77e98
feat(container): implement dependency injection container and provide…
marcuscastelo Dec 6, 2025
0d90b17
refactor(di): batch 1 - user & container wiring
marcuscastelo Dec 6, 2025
3159e61
refactor(di): auth usecases shim
marcuscastelo Dec 6, 2025
323e037
docs(di): add DI migration plan
marcuscastelo Dec 6, 2025
4e300d9
refactor(di): batch 2 - recipe crud factory + shims
marcuscastelo Dec 6, 2025
74c193e
refactor(di): batch 2 - food crud factory + shims
marcuscastelo Dec 6, 2025
1d024d7
refactor(di): batch 2 - meal usecases factory + shim
marcuscastelo Dec 6, 2025
3bbc71c
refactor(di): batch 2 - day and meal typing + meal factory
marcuscastelo Dec 6, 2025
22d36b6
refactor(di): batch 2 - meal uses DayUseCases type
marcuscastelo Dec 6, 2025
77a080e
refactor(di): batch 2 - macro-profile service & usecases factories + …
marcuscastelo Dec 6, 2025
b882ae3
refactor(di): batch 2 - macro-profile factories + shims
marcuscastelo Dec 6, 2025
78e9ad7
refactor(di): batch 2 - dayUseCases factory + shim
marcuscastelo Dec 6, 2025
fad6fb6
refactor(di): batch 2 - dedupe DayUseCases export
marcuscastelo Dec 6, 2025
b47a1fa
refactor(di): update migration progress checklist and document batch …
marcuscastelo Dec 6, 2025
0d22e1f
refactor(di): batch-3 - day-diet & template-search factories + shims …
marcuscastelo Dec 6, 2025
2cdecde
docs(di): mark batch-3 completed in DI-migration-plan.md
marcuscastelo Dec 6, 2025
207f722
refactor(di): batch-4 - weight usecases factory + shim
marcuscastelo Dec 6, 2025
b7247c2
refactor(di): batch-5 - toast manager factory + shim (make testable)
marcuscastelo Dec 6, 2025
5f81d81
refactor(di): batch-5 - recent-food CRUD factory + shim
marcuscastelo Dec 6, 2025
fbba9d1
refactor(di): batch-5 - clipboard usecases factory + shim
marcuscastelo Dec 6, 2025
7a8b615
docs(di): update migration plan with assistant migration snapshot and…
marcuscastelo Dec 6, 2025
85688a5
refactor(di): batch-6 - profile/search/observability factories + shims
marcuscastelo Dec 6, 2025
807ae78
refactor(di): batch-4/5 - weight & measure factories + shims (part of…
marcuscastelo Dec 6, 2025
d2e6cfd
refactor(di): batch-4/5 - measure state + measure crud factory + weig…
marcuscastelo Dec 6, 2025
3f3cdcc
refactor(di): batch-C/B - migrate macro-target & macro-overflow to fa…
marcuscastelo Dec 6, 2025
d313d05
docs(di): update migration checklist - mark batch 4/5/6 items done
marcuscastelo Dec 6, 2025
17708e4
refactor(macro-nutrients): add legacy named exports for backward comp…
marcuscastelo Dec 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
510 changes: 510 additions & 0 deletions DI-migration-plan.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": "pnpm run gen-app-version && vinxi build",
"gen-app-version": "bash ./.scripts/gen-app-version.sh",
"type-check": "tsc --noEmit --skipLibCheck",
"test": "pnpm run gen-app-version && vitest run",
"test": "vitest run",
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test script no longer runs gen-app-version before tests, but other scripts like build still depend on it. If tests rely on the generated app version (e.g., for version assertions or exports), they may fail. Verify that tests don't require the app version to be generated, or document why this change is safe.

Suggested change
"test": "vitest run",
"test": "pnpm run gen-app-version && vitest run",

Copilot uses AI. Check for mistakes.
"fix": "eslint . --fix --cache >/dev/null 2>&1 || exit 0",
"lint": "eslint . --cache",
"flint": "pnpm run fix && pnpm run lint",
Expand Down Expand Up @@ -88,4 +88,4 @@
"vite": "^6.3.5",
"vitest": "^3.2.2"
}
}
}
161 changes: 161 additions & 0 deletions src/di/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { createContext, type JSXElement, useContext } from 'solid-js'

import { createAuthUseCases } from '~/modules/auth/application/usecases/authUseCases'
import { createUserUseCases } from '~/modules/user/application/usecases/userUseCases'
import { createSupabaseUserRepository } from '~/modules/user/infrastructure/supabase/supabaseUserRepository'

/**
* Minimal interfaces for commonly-used use-cases.
* Keep these small and extend as consumers need more functionality.
*/
export type AuthUseCases = {
/**
* Initialize authentication lifecycle (listeners, session restore, etc).
* Should be safe to call multiple times.
*/
initializeAuth: () => void

/**
* Optional helper used for guest detection in legacy flows.
*/
currentUserIdOrGuestId?: () => string | null

[key: string]: unknown
}

export type UserUseCases = {
[key: string]: unknown
}

export type GuestUseCases = {
hasAcceptedGuestTerms?: () => boolean
[key: string]: unknown
}

/**
* Central DI container shape used by UI layer.
*
* Notes:
* - Prefer exposing factories or plain objects; avoid embedding ephemeral UI
* state (signals) inside the container.
* - Tests/SSR can call `createContainer(overrides)` to replace implementations.
*/
export type Container = {
authUseCases: AuthUseCases
userUseCases: UserUseCases
guestUseCases: GuestUseCases

/**
* Optional lifecycle hook for realtime or other infra that must be started
* when the app boots (Providers may call this if present).
*/
initializeWeightRealtime?: () => void

[key: string]: unknown
}

/**
* Create a container with sane defaults. Callers may provide partial overrides
* to replace concrete implementations (useful for tests and SSR).
*
* The returned container is intended to be created once and reused as the
* Context value for the app. Avoid creating a new container on every render.
*/
export function createContainer(
overrides: Partial<Container> = {},
): Readonly<Container> {
// Create default implementations using factories. These defaults are plain
// objects (not signals) and are safe to reuse as the container's defaults.
// Consumers and tests can override any of these via `overrides`.
const defaultUserUseCases = createUserUseCases({
repository: () => createSupabaseUserRepository(),
})

const defaultAuthUseCases: AuthUseCases = createAuthUseCases({
userUseCases: () => defaultUserUseCases,
})

const base: Container = {
authUseCases: defaultAuthUseCases,
userUseCases: defaultUserUseCases,
guestUseCases: {},
initializeWeightRealtime: undefined,
}

// Merge defaults with overrides. The result satisfies `Container`.
const merged: Container = {
...base,
...overrides,
}

// Freeze to discourage accidental mutation at runtime.
Object.freeze(merged)

return merged
}

/**
* Solid context holding the container instance.
* Consumers should call `useContainer()` to access services.
*/
const ContainerContext = createContext<Readonly<Container> | null>(null)

/**
* Provider component that exposes the app container.
*
* Usage:
* const container = createContainer({ authUseCases: myAuth })
* <ContainerProvider value={container}>{children}</ContainerProvider>
*
* Note: pass a stable container instance (do not recreate it on each render).
*/
/* eslint-disable solid/reactivity */
export function ContainerProvider(props: {
value: Readonly<Container>
children?: JSXElement
}) {
return (
<ContainerContext.Provider value={props.value}>
{props.children}
</ContainerContext.Provider>
)
}
/* eslint-enable solid/reactivity */

/**
* Hook to access the current container. Throws when used without a provider.
*
* Prefer passing explicit dependencies into domain/use-case factories rather
* than calling `useContainer()` deep inside pure domain logic.
*/
export function useContainer(): Readonly<Container> {
const ctx = useContext(ContainerContext)
if (!ctx) {
throw new Error(
'Container not provided. Wrap the app with <ContainerProvider value={createContainer(...)}/>.',
)
}
return ctx
}

/**
* Small helper to create a test container quickly. Tests should explicitly
* override only the services they need.
*/
export function createTestContainer(
overrides: Partial<Container> = {},
): Readonly<Container> {
const testAuth: AuthUseCases = {
initializeAuth: () => {
/* no-op */
},
currentUserIdOrGuestId: () => null,
}

return createContainer({
authUseCases: testAuth,
userUseCases: {},
guestUseCases: {},
...overrides,
})
}
18 changes: 18 additions & 0 deletions src/modules/auth/application/usecases/authUseCases.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { type AuthDI, createAuthDI } from '~/modules/auth/application/authDI'
import { userUseCases } from '~/modules/user/application/usecases/userUseCases'
import { GUEST_USER_ID } from '~/shared/guest/guestConstants'

/**
* Factory that creates auth use-cases.
* @param di.userUseCases - provider for user-related use-cases (injected)
*/
export function createAuthUseCases(di: AuthDI) {
const { authStore, authService } = createAuthDI(di)

Expand All @@ -18,3 +23,16 @@ export function createAuthUseCases(di: AuthDI) {
loadInitialSession: () => authService.loadInitialSession(),
}
}

/**
* Public type for the concrete auth use-cases returned by the factory.
*/
export type AuthUseCases = ReturnType<typeof createAuthUseCases>

/**
* Backward-compatible default instance (shim) used by legacy consumers.
* Keeps existing imports working while consumers migrate to the container.
*/
export const authUseCases = createAuthUseCases({
userUseCases: () => userUseCases,
})
Loading