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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.0–friendly 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)
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user