fix(aiui): mock libraries drop out of the production bundle (W1.4)

Every mock consumer is now gated on the demo flag inline (canonical Vite DCE
idiom — the cross-module DEMO_CONTENT_ENABLED const defeated folding). But
the real leak was films.ts's module-level allGenres/allSources exports:
[...new Set(mockFilms.flatMap(...))] is unprovably pure, so the treeshaker
kept the whole module — array, plex:// and cloud.example.com hosts and all —
even with zero live references. The mocks directory is now declared
side-effect-free in vite.config (they are pure data by design), so unneeded
mock modules actually drop.

Verified: clean dist build → entry bundle AND dist-wide grep show zero
mock hosts (spotify/track/example, cloud.example.com, plex://, tmdb image
host). Demo/dev builds (VITE_DEMO_CONTENT=true or import.meta.env.DEV) keep
the full pack. Tests: 353/356, failures are the three documented
pre-existing ones.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 14:06:59 -04:00
co-authored by Claude
parent e669a3e436
commit 8329b826b1
6 changed files with 81 additions and 35 deletions
@@ -4,6 +4,14 @@ import type { MagazineSection } from './contentFiltering'
import { mockFilms } from '@/mocks/films'
import { mockSongs } from '@/mocks/songs'
import { mockPodcasts } from '@/mocks/podcasts'
// Demo-site content pack (operator decision 2026-08-07): [[film:ID]]-style
// library references resolve against the mock libraries in demo/dev builds
// only. In production the tables constant-fold to empty — no mock module
// reaches the shipped bundle — and ext tags (self-contained) keep working.
const libraryFilms: Film[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : []
const librarySongs: Song[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : []
const libraryPodcasts: Podcast[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : []
import { isNewsLikeResponse, isMusicQuery, isTVQuery, isBookQuery, isBookLikeResponse, isImageQuery, isPlaceQuery, isPlaceLikeResponse } from './contentFiltering'
// ─── Tag regexes ──────────────────────────────────────────────────
@@ -437,7 +445,7 @@ export function extractFilmIds(text: string): string[] {
function resolveFilms(ids: string[]): Film[] {
return ids
.map((id) => mockFilms.find((f) => f.id === id))
.map((id) => libraryFilms.find((f) => f.id === id))
.filter((f): f is Film => !!f)
}
@@ -495,7 +503,7 @@ export function extractSongIds(text: string): string[] {
function resolveSongs(ids: string[]): Song[] {
return ids
.map((id) => mockSongs.find((s) => s.id === id))
.map((id) => librarySongs.find((s) => s.id === id))
.filter((s): s is Song => !!s)
}
@@ -560,7 +568,7 @@ function extractSongsFromLibraryMatch(text: string): Song[] {
const lower = text.toLowerCase()
const found: { song: Song; pos: number }[] = []
const seen = new Set<string>()
for (const song of mockSongs) {
for (const song of librarySongs) {
const key = song.id
if (seen.has(key)) continue
const title = song.title.toLowerCase()
@@ -674,7 +682,7 @@ export function extractPodcastIds(text: string): string[] {
function resolvePodcasts(ids: string[]): Podcast[] {
return ids
.map((id) => mockPodcasts.find((p) => p.id === id))
.map((id) => libraryPodcasts.find((p) => p.id === id))
.filter((p): p is Podcast => !!p)
}
+15 -15
View File
@@ -24,17 +24,25 @@ import { mockFilms } from '@/mocks/films'
import { mockSongs } from '@/mocks/songs'
import { mockPodcasts } from '@/mocks/podcasts'
const filmContext = mockFilms.map((f) =>
// Demo-site content pack (operator decision 2026-08-07): mock libraries are
// presented as "the user's library" ONLY in demo/dev builds. In production
// these constant-fold to empty strings, the prompt's library block vanishes,
// and the mock modules drop out of the bundle entirely.
const filmContext = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms.map((f) =>
`- [${f.id}] "${f.title}" (${f.year}) dir. ${f.director} | ${f.genres.join(', ')} | ${f.rating}/10 | On: ${f.sources.map(s => s.type).join(', ')}`
).join('\n')
).join('\n') : ''
const songContext = mockSongs.map((s) =>
const songContext = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs.map((s) =>
`- [${s.id}] "${s.title}" by ${s.artist}${s.album ? ` (${s.album})` : ''}${s.year ? ` (${s.year})` : ''} | ${(s.genres ?? []).join(', ')} | On: ${(s.sources ?? []).map(x => x.type).join(', ')}`
).join('\n')
).join('\n') : ''
const podcastContext = mockPodcasts.map((p) =>
const podcastContext = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts.map((p) =>
`- [${p.id}] "${p.title}" by ${p.host ?? 'Unknown'}${p.year ? ` (${p.year})` : ''} | ${(p.genres ?? []).join(', ')} | On: ${p.sources.map(x => x.type).join(', ')}`
).join('\n')
).join('\n') : ''
const librarySection = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true')
? `\nThe user's film library:\n${filmContext}\n\nThe user's song library:\n${songContext}\n\nThe user's podcast library:\n${podcastContext}`
: ''
// ─── Wavlake catalog context (fetched at runtime) ────────────
interface WavlakeCatalogTrack {
@@ -108,15 +116,7 @@ Prioritize Podcasting 2.0friendly platforms: Fountain.fm, Podcast Index, Cast
**Music discovery:** All music plays from **Wavlake** — a Lightning-powered, Nostr-native music platform. When recommending songs, prefer tracks from the Wavlake trending list (provided below) since those are confirmed playable. For genre requests, use [[song_ext:Title|Artist]] tags — the UI will search Wavlake automatically. Songs not on Wavlake won't play, so stick to Wavlake artists when you can. The user can zap (tip) artists with Lightning directly through the platform.
Always include these tags so the UI can render rich cards. Write a brief reason why each is worth checking out.
The user's film library:
${filmContext}
The user's song library:
${songContext}
The user's podcast library:
${podcastContext}`
${librarySection}`
const activeProvider = ref<Provider>('claude')
@@ -5,6 +5,12 @@ import type { WebSearchResult } from '@aiui/core/types/message'
import { mockFilms } from '@/mocks/films'
import { mockSongs } from '@/mocks/songs'
import { mockPodcasts } from '@/mocks/podcasts'
// Demo-site content pack (operator decision 2026-08-07): the library views
// fill from mocks in demo/dev builds only; production folds these to empty.
const demoFilms: Film[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : []
const demoSongs: Song[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : []
const demoPodcasts: Podcast[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : []
import { fetchRssFromUrls } from '@/composables/useRssFetch'
import {
extractAllFilms, extractAllSongs, extractAllPodcasts, extractAllBooks,
@@ -592,7 +598,7 @@ export function useContentPanel() {
}
function showAllFilms() {
panelFilms.value = [...mockFilms]
panelFilms.value = [...demoFilms]
panelSongs.value = []
panelPodcasts.value = []
panelTitle.value = 'Your Film Library'
@@ -603,7 +609,7 @@ export function useContentPanel() {
function showAllSongs() {
panelFilms.value = []
panelSongs.value = [...mockSongs]
panelSongs.value = [...demoSongs]
panelPodcasts.value = []
panelTitle.value = 'Your Song Library'
contentType.value = 'song'
@@ -614,7 +620,7 @@ export function useContentPanel() {
function showAllPodcasts() {
panelFilms.value = []
panelSongs.value = []
panelPodcasts.value = [...mockPodcasts]
panelPodcasts.value = [...demoPodcasts]
panelTitle.value = 'Your Podcast Library'
contentType.value = 'podcast'
panelOpen.value = true
@@ -2,6 +2,12 @@ import { ref, watch } from 'vue'
import { mockFilms } from '@/mocks/films'
import { mockSongs } from '@/mocks/songs'
import { mockPodcasts } from '@/mocks/podcasts'
import type { Film, Song, Podcast } from '@aiui/core/types/content'
// Demo-site content pack (operator decision 2026-08-07): demo/dev only.
const demoFilms: Film[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : []
const demoSongs: Song[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : []
const demoPodcasts: Podcast[] = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : []
export interface SearchResult {
type: 'film' | 'song' | 'podcast'
@@ -21,7 +27,7 @@ function searchLibrary(q: string): SearchResult[] {
const lower = q.toLowerCase()
const matched: SearchResult[] = []
for (const film of mockFilms) {
for (const film of demoFilms) {
if (
film.title.toLowerCase().includes(lower) ||
film.director.toLowerCase().includes(lower) ||
@@ -37,7 +43,7 @@ function searchLibrary(q: string): SearchResult[] {
}
}
for (const song of mockSongs) {
for (const song of demoSongs) {
if (
song.title.toLowerCase().includes(lower) ||
song.artist.toLowerCase().includes(lower) ||
@@ -53,7 +59,7 @@ function searchLibrary(q: string): SearchResult[] {
}
}
for (const podcast of mockPodcasts) {
for (const podcast of demoPodcasts) {
if (
podcast.title.toLowerCase().includes(lower) ||
(podcast.host ?? '').toLowerCase().includes(lower)
+14 -9
View File
@@ -6,6 +6,11 @@ import { mockFilms } from '@/mocks/films'
import { mockSongs } from '@/mocks/songs'
import { mockPodcasts } from '@/mocks/podcasts'
// Demo-site content pack (operator decision 2026-08-07): demo/dev only.
const demoFilms = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockFilms : []
const demoSongs = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockSongs : []
const demoPodcasts = (import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true') ? mockPodcasts : []
export interface MCPTool {
name: string
description: string
@@ -97,7 +102,7 @@ type ToolHandler = (input: Record<string, unknown>) => unknown
const toolHandlers: Record<string, ToolHandler> = {
search_films(input) {
const query = (input.query as string ?? '').toLowerCase()
const results = mockFilms.filter(f =>
const results = demoFilms.filter(f =>
f.title.toLowerCase().includes(query) ||
f.director.toLowerCase().includes(query) ||
f.genres.some(g => g.toLowerCase().includes(query)),
@@ -117,7 +122,7 @@ const toolHandlers: Record<string, ToolHandler> = {
search_songs(input) {
const query = (input.query as string ?? '').toLowerCase()
const results = mockSongs.filter(s =>
const results = demoSongs.filter(s =>
s.title.toLowerCase().includes(query) ||
s.artist.toLowerCase().includes(query) ||
(s.genres ?? []).some(g => g.toLowerCase().includes(query)),
@@ -137,7 +142,7 @@ const toolHandlers: Record<string, ToolHandler> = {
search_podcasts(input) {
const query = (input.query as string ?? '').toLowerCase()
const results = mockPodcasts.filter(p =>
const results = demoPodcasts.filter(p =>
p.title.toLowerCase().includes(query) ||
(p.host ?? '').toLowerCase().includes(query) ||
(p.genres ?? []).some(g => g.toLowerCase().includes(query)),
@@ -156,13 +161,13 @@ const toolHandlers: Record<string, ToolHandler> = {
get_library_stats() {
return {
films: mockFilms.length,
songs: mockSongs.length,
podcasts: mockPodcasts.length,
films: demoFilms.length,
songs: demoSongs.length,
podcasts: demoPodcasts.length,
genres: {
film: [...new Set(mockFilms.flatMap(f => f.genres))].sort(),
song: [...new Set(mockSongs.flatMap(s => s.genres ?? []))].sort(),
podcast: [...new Set(mockPodcasts.flatMap(p => p.genres ?? []))].sort(),
film: [...new Set(demoFilms.flatMap(f => f.genres))].sort(),
song: [...new Set(demoSongs.flatMap(s => s.genres ?? []))].sort(),
podcast: [...new Set(demoPodcasts.flatMap(p => p.genres ?? []))].sort(),
},
}
},
+22 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { VitePWA } from 'vite-plugin-pwa'
import { resolve } from 'path'
import { resolve, sep } from 'path'
import { tmdbPlugin } from './vite-tmdb'
import { devChatsPlugin } from './vite-dev-chats'
import { musicSearchPlugin } from './vite-music-search'
@@ -12,6 +12,14 @@ import { fsPlugin } from './vite-fs'
export default defineConfig({
base: process.env.VITE_BASE_PATH || '/',
// Always define VITE_DEMO_CONTENT as a string literal so the
// DEMO_CONTENT_ENABLED gate constant-folds at build time (an UNDEFINED
// env var leaves `undefined === 'true'` in the bundle, which rollup's
// treeshaker refuses to evaluate — that is how mock hosts survived in
// the 2026-08-07 RC1 prod bundle despite the gate).
define: {
'import.meta.env.VITE_DEMO_CONTENT': JSON.stringify(process.env.VITE_DEMO_CONTENT ?? 'false'),
},
plugins: [
vue(),
tailwindcss(),
@@ -129,6 +137,19 @@ export default defineConfig({
'@aiui/core': resolve(__dirname, '../core/src'),
},
},
build: {
rollupOptions: {
treeshake: {
// Mock modules are pure data by design — but a module-level
// `allGenres = [...new Set(mockFilms.flatMap(...))]` export made
// films.ts look side-effectful, so the treeshaker kept the whole
// module (and every mock host string) in the prod bundle even with
// zero live references. Declaring the mocks directory
// side-effect-free lets the demo-content gate actually drop them.
moduleSideEffects: (id) => !id.includes(`${sep}src${sep}mocks${sep}`),
},
},
},
server: {
port: 5173,
host: process.env.VITE_HOST === 'false' ? 'localhost' : true,