Files
app/vite.config.ts
zeekayandHanzo Dev 3e5f255024 The host says which DAO this is
lux.vote, zoo.vote and hanzo.vote are one bundle. Which one a document is was a
build-time constant, so serving three sites meant three builds and a fourth way
to get it wrong: an image reached by the wrong name reports real figures under
somebody else's brand, which is the one failure a reader cannot detect.

The hostname settles it instead. Every label is checked against BRANDS, so
lux.vote, www.hanzo.vote and vote.zoo.network all reach the right chain from the
same bytes. VITE_VOTE_HOME stays as what a host belonging to nobody gets —
localhost, an IP, a preview URL — which is also what each image is built with,
so a container reached by an unbranded name is still itself.

A tenant is not a chain, and the two now live apart. gov/chain.ts says which
chains are readable; every site reaches all of them through the picker.
chrome/brand.tsx says which chains have a site, what it is called and what it is
drawn with. HOME answers the second question, so it moved there — and it must
not be re-exported from chain.ts, because brand.tsx imports venue() and ESM
hoists a re-export, which evaluates brand.tsx first against VENUES in its
temporal dead zone and throws on every screen.

The lockup is the mark plus what the mark does not already say. The Lux mark is
the letters L and X, so "Vote" follows it; the Zoo and Hanzo marks are glyphs
and carry their names. Zoo's is the monochrome cut from zoo/logo, Hanzo's the
block H from @hanzo/gui — both inline and both currentColor, for the reasons
lx.tsx already gave for Lux's, which is why that file is now this one.

Pars is a readable chain and not a tenant. pars.vote is served from a fork,
which extends BRANDS rather than editing it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-04 00:15:11 -07:00

87 lines
3.9 KiB
TypeScript

import { readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import react from '@vitejs/plugin-react'
import { bootScript } from '@hanzo/appearance/state'
// The `.js` is deliberate. Vite's config loader wants an extension and
// TypeScript refuses a `.ts` one unless allowImportingTsExtensions is on;
// `.js` is what ESM asks a TypeScript source to be imported as.
import { boot } from './src/chrome/boot.js'
import { defineConfig, type Plugin } from 'vite'
/**
* Every engine package, deduplicated.
*
* The design system draws through one module registry. Two physical copies of
* any of these means two registries, and the symptom names nothing — a popover
* throwing on an undefined reference, a theme that resolves on one component
* and not its sibling. Read from disk rather than listed, so a package added by
* an upgrade is deduplicated without anyone remembering to add it here.
*/
const engine = readdirSync(fileURLToPath(new URL('./node_modules/@hanzogui', import.meta.url)))
.filter((d) => !d.startsWith('.'))
.map((d) => `@hanzogui/${d}`)
/**
* Puts the two settings that must precede first paint in <head>, ahead of the
* bundle. A person's type scale and the ground the page is drawn on are a
* custom property and a class on <html>; React mounts after first paint, so the
* bundle cannot set either soon enough and the page would draw once at the
* published reading and again at theirs.
*/
function head(): Plugin {
return {
name: 'head-settings',
transformIndexHtml: () => [
{ tag: 'script', injectTo: 'head' as const, children: bootScript() },
{ tag: 'script', injectTo: 'head' as const, children: boot() },
],
}
}
// The tenant this build falls back to, checked here rather than only in the app.
//
// brand.tsx throws on an unknown key, but that throw lands in a browser: the
// bundle builds, publishes, deploys, and shows a blank page. The keys are known
// at config time, so a typo should cost a failed build and not a failed site.
// Kept in step with BRANDS in src/chrome/brand.tsx.
const HOMES = ['lux', 'zoo', 'hanzo'] as const
const home = process.env.VITE_VOTE_HOME ?? 'lux'
if (!HOMES.includes(home as (typeof HOMES)[number])) {
throw new Error(`VITE_VOTE_HOME names "${home}", which is not a tenant. Known: ${HOMES.join(', ')}.`)
}
export default defineConfig(({ mode }) => ({
plugins: [react(), head()],
define: {
__DEV__: mode !== 'production',
'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'),
// The design system ships analytics that post to api.hanzo.ai on load, on
// by default. This is a governance interface: who reads a proposal, and
// when, is exactly the kind of thing it has no business reporting. `off` is
// the switch @hanzogui/telemetry already publishes, set here rather than in
// a .env so the decision is visible in the build and cannot be lost by a
// missing file.
'import.meta.env.VITE_HANZO_TELEMETRY': JSON.stringify('off'),
},
resolve: {
// The engine is cross-platform and reaches for react-native at module
// scope. In a browser bundle that has to land on the web build.
alias: { 'react-native': 'react-native-web' },
dedupe: ['react', 'react-dom', 'react-native-web', '@hanzo/gui', ...engine],
},
// The engine and its web shim reach React through CommonJS. Split into their
// own chunk they resolve a different copy and `createContext` is undefined at
// first paint.
build: { commonjsOptions: { include: [/node_modules/] } },
optimizeDeps: {
include: ['react-native-web', '@react-native/normalize-color', '@hanzo/gui', '@hanzogui/core'],
},
server: {
port: 5288,
// If the port is taken, stop rather than move. A dev server that quietly
// picks another port is how two builds end up served at once and the one
// being read is not the one being edited.
strictPort: true,
},
}))