Files
id/apps/web/vite.config.ts
zooqueen f23ab3d670 fix(id): emit per-brand brand.json in build (ESM require) + harden loadBrand
Root cause of the post-cutover white-screen: vite.config.ts is loaded as a
native ESM module where `require` is undefined, so the brandJsonPlugin's
`require.resolve('<pkg>/brand.json')` threw and the silent catch skipped
emitting EVERY brand.json. The SPA then served index.html (HTTP 200,
text/html) for /brand/<pkg>/brand.json, loadBrand did res.json() on HTML,
threw "Unexpected token '<'", and App's catch rendered that error as the
whole page — no login form ever mounted.

- vite.config.ts: build a createRequire(import.meta.url) resolver so the
  brand.json subpath resolves under ESM. Build now emits all four
  dist/brand/<pkg>/brand.json (verified: @hanzo brand.name = "Hanzo").
- brand.ts: read the response as text and JSON.parse explicitly; on
  non-JSON throw a precise diagnostic instead of a cryptic SyntaxError,
  and require the "brand" key.
- apps/web 0.1.8 -> 0.1.9.
2026-06-19 10:39:53 -07:00

85 lines
2.6 KiB
TypeScript

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
import { readFileSync, existsSync } from 'fs'
import { createRequire } from 'module'
// Vite loads this config as a native ESM module, where `require` is undefined.
// Build a CJS-style resolver bound to this file so `<pkg>/brand.json` subpath
// resolution works in both `configureServer` and `generateBundle`.
const require = createRequire(import.meta.url)
/**
* Per-brand brand.json copy plugin.
*
* Each per-org brand package (`@hanzo/brand`, `@luxfi/brand`, `@zooai/brand`,
* `@parsdao/brand`) ships a `brand.json` at the package root. We serve them
* verbatim at `/brand/<pkg>/brand.json` so the runtime tenant resolver can
* fetch the right one based on hostname.
*
* Assets (logos, favicons) are imported by URL inside the per-brand `brand.json`
* (CDN URLs in production), so no further asset copying is needed.
*/
const BRAND_PACKAGES = ['@hanzo/brand', '@luxfi/brand', '@zooai/brand', '@parsdao/brand']
function brandJsonPlugin() {
return {
name: 'hanzo-id-brand-json',
configureServer(server: any) {
server.middlewares.use((req: any, res: any, next: any) => {
const m = /^\/brand\/(.+)\/brand\.json$/.exec(req.url ?? '')
if (!m) return next()
const pkg = decodeURIComponent(m[1]!)
if (!BRAND_PACKAGES.includes(pkg)) {
res.statusCode = 404
return res.end()
}
try {
const path = require.resolve(`${pkg}/brand.json`)
res.setHeader('Content-Type', 'application/json')
res.setHeader('Cache-Control', 'no-store')
return res.end(readFileSync(path, 'utf8'))
} catch {
res.statusCode = 404
return res.end()
}
})
},
generateBundle(this: any) {
for (const pkg of BRAND_PACKAGES) {
try {
const path = require.resolve(`${pkg}/brand.json`)
if (!existsSync(path)) continue
this.emitFile({
type: 'asset',
fileName: `brand/${pkg}/brand.json`,
source: readFileSync(path, 'utf8'),
})
} catch {
// pkg not installed — skip silently; only the brands listed in
// package.json deps actually need their JSON shipped.
}
}
},
}
}
export default defineConfig({
plugins: [react(), brandJsonPlugin()],
resolve: {
alias: { '@': resolve(__dirname, 'src') },
},
server: {
port: 5173,
host: '0.0.0.0',
},
preview: {
port: 5174,
host: '0.0.0.0',
},
build: {
target: 'es2022',
sourcemap: true,
},
})