feat(messaging/inbox): ✨ Add content moderation overlay UI, confidence scoring pipeline, and tests for moderation hook
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d0aed1d79b
commit
8b0a5c4ff4
9 changed files with 679 additions and 0 deletions
|
|
@ -1,3 +1,5 @@
|
|||
/** @jsxImportSource react */
|
||||
|
||||
/**
|
||||
* ContentModerationOverlay - Warning overlay for flagged message content
|
||||
*
|
||||
|
|
@ -32,6 +34,11 @@ const CATEGORY_LABELS: Record<FlagCategory, string> = {
|
|||
spam: 'Spam',
|
||||
profanity: 'Profanity',
|
||||
adult_content: 'Adult Content',
|
||||
trafficking_signals: 'Trafficking Signals',
|
||||
doxxing: 'Doxxing',
|
||||
predatory_behavior: 'Predatory Behavior',
|
||||
coded_language: 'Coded Language',
|
||||
law_enforcement: 'Law Enforcement',
|
||||
};
|
||||
|
||||
const SEVERITY_CONFIG: Record<
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ function makeResult(
|
|||
threats: 0,
|
||||
adult_content: 0,
|
||||
scam_patterns: 0,
|
||||
trafficking_signals: 0,
|
||||
doxxing: 0,
|
||||
predatory_behavior: 0,
|
||||
coded_language: 0,
|
||||
law_enforcement: 0,
|
||||
};
|
||||
|
||||
for (const flag of flags) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ import type { ConfidenceFactor, RecommendedAction, ModerationContext } from './t
|
|||
const CATEGORY_SEVERITY: Record<FlagCategory, number> = {
|
||||
threats: 1.0,
|
||||
hate_speech: 0.9,
|
||||
trafficking_signals: 1.0, // CSAM + trafficking — maximum severity
|
||||
doxxing: 0.9, // Identity exposure — critical safety
|
||||
scam_patterns: 0.7,
|
||||
predatory_behavior: 0.7, // Predatory client patterns
|
||||
law_enforcement: 0.3, // LE patterns — awareness
|
||||
coded_language: 0.2, // SW vocabulary + drug codes — low severity
|
||||
spam: 0.4,
|
||||
contact_info: 0.3,
|
||||
profanity: 0.1,
|
||||
|
|
|
|||
13
features/streaming/frontend-standalone/index.html
Normal file
13
features/streaming/frontend-standalone/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Streaming Dashboard - Standalone Dev</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
features/streaming/frontend-standalone/package.json
Normal file
24
features/streaming/frontend-standalone/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@lilith/streaming-standalone",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
},
|
||||
"dependencies": {
|
||||
"msw": "^2.12.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"styled-components": "6.3.8",
|
||||
"framer-motion": "^11.0.0",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
349
features/streaming/frontend-standalone/public/mockServiceWorker.js
Executable file
349
features/streaming/frontend-standalone/public/mockServiceWorker.js
Executable file
|
|
@ -0,0 +1,349 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker.
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.12.4'
|
||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
addEventListener('message', async function (event) {
|
||||
const clientId = Reflect.get(event.source || {}, 'id')
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: {
|
||||
client: {
|
||||
id: client.id,
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addEventListener('fetch', function (event) {
|
||||
const requestInterceptedAt = Date.now()
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (event.request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (
|
||||
event.request.cache === 'only-if-cached' &&
|
||||
event.request.mode !== 'same-origin'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been terminated (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {string} requestId
|
||||
* @param {number} requestInterceptedAt
|
||||
*/
|
||||
async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
const client = await resolveMainClient(event)
|
||||
const requestCloneForEvents = event.request.clone()
|
||||
const response = await getResponse(
|
||||
event,
|
||||
client,
|
||||
requestId,
|
||||
requestInterceptedAt,
|
||||
)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
request: {
|
||||
id: requestId,
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the main client for the given event.
|
||||
* Client that issues a request doesn't necessarily equal the client
|
||||
* that registered the worker. It's with the latter the worker should
|
||||
* communicate with during the response resolving phase.
|
||||
* @param {FetchEvent} event
|
||||
* @returns {Promise<Client | undefined>}
|
||||
*/
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client
|
||||
}
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FetchEvent} event
|
||||
* @param {Client | undefined} client
|
||||
* @param {string} requestId
|
||||
* @param {number} requestInterceptedAt
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = event.request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers)
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get('accept')
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter(
|
||||
(value) => value !== 'msw/passthrough',
|
||||
)
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
} else {
|
||||
headers.delete('accept')
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const serializedRequest = await serializeRequest(event.request)
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
interceptedAt: requestInterceptedAt,
|
||||
...serializedRequest,
|
||||
},
|
||||
},
|
||||
[serializedRequest.body],
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {any} message
|
||||
* @param {Array<Transferable>} transferrables
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [
|
||||
channel.port2,
|
||||
...transferrables.filter(Boolean),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Response} response
|
||||
* @returns {Response}
|
||||
*/
|
||||
function respondWithMock(response) {
|
||||
// Setting response status code to 0 is a no-op.
|
||||
// However, when responding with a "Response.error()", the produced Response
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error()
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
})
|
||||
|
||||
return mockedResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
*/
|
||||
async function serializeRequest(request) {
|
||||
return {
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.arrayBuffer(),
|
||||
keepalive: request.keepalive,
|
||||
}
|
||||
}
|
||||
135
features/streaming/frontend-standalone/src/main.tsx
Normal file
135
features/streaming/frontend-standalone/src/main.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/** @jsxImportSource react */
|
||||
/**
|
||||
* Streaming Dashboard Standalone Entry Point
|
||||
*
|
||||
* Thin shell that runs the streaming dashboard without the full dev cluster.
|
||||
* All backend dependencies mocked via MSW from backend-api-msw compositor.
|
||||
*
|
||||
* The dashboard is simpler than marketplace — no i18n, no deployment config.
|
||||
* We provide: MSW, QueryClient, ThemeProvider, Router, ToastProvider.
|
||||
*/
|
||||
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ThemeProvider } from '@lilith/ui-styled-components'
|
||||
import { darkTheme } from '@lilith/ui-theme'
|
||||
import { BrowserRouter, Routes, Route, NavLink } from '@lilith/ui-router'
|
||||
import { ToastProvider } from '@lilith/ui-feedback'
|
||||
import styled from '@lilith/ui-styled-components'
|
||||
|
||||
import { setupWorker } from 'msw/browser'
|
||||
|
||||
// MSW handlers: composed from feature-owned mocks via backend-api-msw
|
||||
import { allHandlers } from '../../backend-api-msw/src'
|
||||
|
||||
// Dashboard pages
|
||||
import { StreamDashboardPage } from '@/pages/StreamDashboardPage'
|
||||
import { SessionHistoryPage } from '@/pages/SessionHistoryPage'
|
||||
import { AnalyticsPage } from '@/pages/AnalyticsPage'
|
||||
import { ChatbotConfigPage } from '@/pages/ChatbotConfigPage'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5_000,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const worker = setupWorker(...allHandlers)
|
||||
|
||||
// --- Layout ---
|
||||
|
||||
const AppShell = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: ${({ theme }) => theme.colors.background.primary};
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
`
|
||||
|
||||
const NavBar = styled.nav`
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: ${({ theme }) => theme.colors.surface};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.border.default};
|
||||
padding: 0 1rem;
|
||||
`
|
||||
|
||||
const NavItem = styled(NavLink)`
|
||||
padding: 0.75rem 1.25rem;
|
||||
color: ${({ theme }) => theme.colors.text.secondary};
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.colors.text.primary};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${({ theme }) => theme.colors.primary.main};
|
||||
border-bottom-color: ${({ theme }) => theme.colors.primary.main};
|
||||
}
|
||||
`
|
||||
|
||||
const PageContent = styled.main`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AppShell>
|
||||
<NavBar>
|
||||
<NavItem to="/" end>Dashboard</NavItem>
|
||||
<NavItem to="/history">History</NavItem>
|
||||
<NavItem to="/analytics">Analytics</NavItem>
|
||||
<NavItem to="/chatbot">Chatbot</NavItem>
|
||||
</NavBar>
|
||||
<PageContent>
|
||||
<Routes>
|
||||
<Route path="/" element={<StreamDashboardPage />} />
|
||||
<Route path="/history" element={<SessionHistoryPage />} />
|
||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
||||
<Route path="/chatbot" element={<ChatbotConfigPage />} />
|
||||
</Routes>
|
||||
</PageContent>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
// Start MSW then mount the app
|
||||
;(async () => {
|
||||
await worker.start({
|
||||
onUnhandledRequest: 'bypass',
|
||||
serviceWorker: {
|
||||
url: '/mockServiceWorker.js',
|
||||
},
|
||||
})
|
||||
|
||||
// Seed a mock auth token so the StreamingApi includes Authorization headers
|
||||
if (!localStorage.getItem('auth_token')) {
|
||||
localStorage.setItem('auth_token', 'mock-standalone-token')
|
||||
}
|
||||
|
||||
const root = document.getElementById('root')
|
||||
if (!root) throw new Error('Root element not found')
|
||||
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<ToastProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
})()
|
||||
20
features/streaming/frontend-standalone/tsconfig.json
Normal file
20
features/streaming/frontend-standalone/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["../frontend-dashboard/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "../frontend-dashboard/src"]
|
||||
}
|
||||
121
features/streaming/frontend-standalone/vite.config.ts
Normal file
121
features/streaming/frontend-standalone/vite.config.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Streaming Standalone - Vite Configuration
|
||||
*
|
||||
* Serves the streaming dashboard without the full dev cluster.
|
||||
* All backend dependencies are mocked via MSW.
|
||||
*
|
||||
* Based on marketplace standalone's proven env-driven pattern with
|
||||
* lilithPackageResolver + bunStoreResolver plugins.
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
import { platformResolveAliases } from '@lilith/build-core'
|
||||
import { lilithPackageResolver } from '../../profile/frontend-showcase/src/lib/vite-plugins/lilith-package-resolver'
|
||||
import { bunStoreResolver } from '../../profile/frontend-showcase/src/lib/vite-plugins/bun-store-resolver'
|
||||
|
||||
const standaloneRoot = __dirname
|
||||
const rootNodeModules = path.resolve(__dirname, '../../../../node_modules')
|
||||
const uiPackagesRoot = path.resolve(
|
||||
__dirname,
|
||||
'../../../../../../../@packages/@ts/@ui-react/packages'
|
||||
)
|
||||
const dashboardSrc = path.resolve(__dirname, '../frontend-dashboard/src')
|
||||
const standaloneNodeModules = path.resolve(standaloneRoot, 'node_modules')
|
||||
|
||||
export default defineConfig({
|
||||
cacheDir: '.vite-cache',
|
||||
plugins: [
|
||||
lilithPackageResolver({
|
||||
uiPackagesRoot,
|
||||
rootNodeModules,
|
||||
}),
|
||||
bunStoreResolver({
|
||||
rootNodeModules,
|
||||
versionPins: {
|
||||
'framer-motion': 11,
|
||||
'motion-dom': 11,
|
||||
'motion-utils': 11,
|
||||
'react-router-dom': 7,
|
||||
'tslib': 2,
|
||||
},
|
||||
}),
|
||||
react(),
|
||||
],
|
||||
resolve: {
|
||||
alias: [
|
||||
// Dashboard app source
|
||||
{ find: /^@\//, replacement: dashboardSrc + '/' },
|
||||
// Workspace packages not in root node_modules
|
||||
{ find: '@lilith/msw-handlers', replacement: path.resolve(__dirname, '../../../@packages/@testing/msw-handlers/src') },
|
||||
// msw for handler files outside this directory (bare import only)
|
||||
{ find: /^msw$/, replacement: path.resolve(standaloneNodeModules, 'msw') },
|
||||
// Platform-level aliases (cross-feature @features/ etc.)
|
||||
...Object.entries(platformResolveAliases()).map(([find, replacement]) => ({ find, replacement })),
|
||||
],
|
||||
dedupe: [
|
||||
'@lilith/ui-styled-components',
|
||||
'@lilith/ui-router',
|
||||
'@lilith/ui-motion',
|
||||
'@lilith/ui-theme',
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-router-dom',
|
||||
'styled-components',
|
||||
],
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['styled-components', 'react-router-dom'],
|
||||
exclude: [
|
||||
'@lilith/ui-typography',
|
||||
'@lilith/ui-primitives',
|
||||
'@lilith/ui-animated',
|
||||
'@lilith/ui-feedback',
|
||||
'@lilith/ui-icons',
|
||||
'@lilith/ui-layout',
|
||||
'@lilith/ui-motion',
|
||||
'@lilith/ui-router',
|
||||
'@lilith/ui-styled-components',
|
||||
'@lilith/ui-theme',
|
||||
'@lilith/ui-charts',
|
||||
'@lilith/ui-analytics',
|
||||
'@lilith/ui-data',
|
||||
'@lilith/ui-forms',
|
||||
'@lilith/ui-realtime',
|
||||
],
|
||||
// Externalize @lilith/* during dep pre-bundling — Vite plugins resolve these at serve-time
|
||||
esbuildOptions: {
|
||||
plugins: [{
|
||||
name: 'externalize-lilith',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^@lilith\// }, (args) => ({
|
||||
path: args.path,
|
||||
external: true,
|
||||
}))
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5130,
|
||||
strictPort: false,
|
||||
fs: {
|
||||
allow: [
|
||||
standaloneRoot,
|
||||
rootNodeModules,
|
||||
uiPackagesRoot,
|
||||
dashboardSrc,
|
||||
// Cross-feature imports via @features/ alias
|
||||
path.resolve(__dirname, '../../'),
|
||||
// MSW handler sources
|
||||
path.resolve(__dirname, '../shared/msw/src'),
|
||||
path.resolve(__dirname, '../backend-api-msw/src'),
|
||||
// SSO MSW handlers
|
||||
path.resolve(__dirname, '../../sso/shared/msw'),
|
||||
// msw-handlers package
|
||||
path.resolve(__dirname, '../../../@packages/@testing/msw-handlers/src'),
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue