Three routes, per architecture/generic.md §4 (Vite + React + SWC + TS,
static, served by nginx): / renders the repository readme, /models is a
paged and filterable listing of what the bucket holds, and
/{namespace}/{name} shows one repository -- its pinned refs, the files
actually stored, and its model card.
The API it reads is /v1/, deliberately not /api/. That surface is the
Hub's, recorded and replayed verbatim, and adding routes of our own to
it risks a client mistaking one for the real thing. /v1/ answers a
question the Hub has no equivalent for -- what is in this bucket --
which is inventory, not the model search the spec rules out.
The listing shows only what has actually been fetched, never upstream's
siblings. A repository pulled one file at a time shows one file, which
is the honest answer to "what can I get from here offline".
Model cards are third-party content, so their HTML is parsed and then
sanitised against GitHub's allowlist. The plugin order is load-bearing
and commented as such. Verified against a card crafted with <script>,
onerror, a javascript: href, an <iframe>, an SVG-embedded script and an
inline handler: none execute and ordinary markdown still renders. Card
images are not loaded at all -- fetching them would leak the viewer's
address to a third party and make an offline registry's pages depend on
the internet.
Routing: rustingface's URL space is the Hub's, so /Qwen/Qwen3-0.6B is
both a page and the prefix of a file. The vhost splits them the way the
Hub does -- /resolve/ anywhere in the path, plus /api/ and /v1/, go to
the service; everything else is the app. A repo legitimately named
v1/repos is handed back to the resolve path by the router, and there is
a test for it. The /resolve/ test is on the repo type rather than the
path substring, because a repo may contain a directory called resolve
and /api/models/a/b/tree/main/resolve/f must stay a tree request.
Deployment: the frontend ships to hanzalova:/var/www/rustingface, so
that host now gets its own scoped gitea_ci drop-in -- narrower than the
service host's: a webroot rsync, a relabel, nginx -t and a reload. The
health check probes rf.internal from the proxy rather than from the
runner, because a runner is a plain Fedora container with no internal
root CA (verified: fedora:43 gets 000, the proxy gets 200).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
63 lines
2.2 KiB
TypeScript
63 lines
2.2 KiB
TypeScript
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react-swc'
|
|
import { readFileSync } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const repoRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
const devApi = process.env.VITE_DEV_API ?? 'http://127.0.0.1:20482'
|
|
|
|
/**
|
|
* Expose the repository readme to the app as a virtual module.
|
|
*
|
|
* The landing page is the readme, so that there is exactly one description of
|
|
* what this service is: a copy under `web/` would drift from the real one, and
|
|
* the drift would be invisible until somebody noticed the site disagreeing
|
|
* with the repository.
|
|
*/
|
|
function repoReadme() {
|
|
const id = 'virtual:repo-readme'
|
|
const resolved = '\0' + id
|
|
return {
|
|
name: 'repo-readme',
|
|
resolveId: (source: string) => (source === id ? resolved : null),
|
|
load(this: { addWatchFile?: (f: string) => void }, requested: string) {
|
|
if (requested !== resolved) return null
|
|
const path = repoRoot + 'readme.md'
|
|
this.addWatchFile?.(path)
|
|
return `export default ${JSON.stringify(readFileSync(path, 'utf8'))}`
|
|
},
|
|
}
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [react(), repoReadme()],
|
|
build: {
|
|
outDir: 'dist',
|
|
sourcemap: false,
|
|
rollupOptions: {
|
|
output: {
|
|
// Split the dependencies that never change from the app that does, so
|
|
// a redeploy does not invalidate a viewer's cached copy of React and
|
|
// the markdown stack.
|
|
manualChunks: {
|
|
react: ['react', 'react-dom', 'react-router-dom'],
|
|
markdown: ['react-markdown', 'remark-gfm', 'rehype-raw', 'rehype-sanitize'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
server: {
|
|
// Dev server only: proxy to a running rustingface so the app is exercised
|
|
// against the real thing rather than a fixture. Mirrors what the nginx
|
|
// vhost does in production — including the resolve paths, which is how
|
|
// model cards are read.
|
|
proxy: {
|
|
'/v1': { target: devApi, changeOrigin: true },
|
|
'/api': { target: devApi, changeOrigin: true },
|
|
'/healthz': { target: devApi, changeOrigin: true },
|
|
// `/{namespace}/{name}/resolve/...` and its bare-name form.
|
|
'^/[^/]+/(?:[^/]+/)?resolve/': { target: devApi, changeOrigin: true },
|
|
},
|
|
},
|
|
})
|