mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
113 lines
5.3 KiB
JavaScript
113 lines
5.3 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
// The build id is written into every document and names the route manifests
|
|
// (_next/static/<id>/_buildManifest.js, _ssgManifest.js). It is derived from what
|
|
// decides the runtime (the resolved dependencies and this file), so a build that
|
|
// changes a page keeps it and an incremental publish sends only the pages that
|
|
// changed instead of every document.
|
|
//
|
|
// The rule that makes that safe: the two manifests answer `no-cache` at the edge
|
|
// (universe charts/app/values/hanzo/static-sites.yaml, the *-manifest routes), so
|
|
// a stable URL is always revalidated; everything else under _next/static is
|
|
// content-hashed and immutable. Both halves must hold. A host that serves the
|
|
// manifests with a long TTL keeps an old manifest beside new HTML, and chunks then
|
|
// load as "<n>-undefined.js".
|
|
const buildId = createHash('sha256')
|
|
.update(readFileSync(new URL('./pnpm-lock.yaml', import.meta.url)))
|
|
.update(readFileSync(new URL(import.meta.url)))
|
|
.digest('hex')
|
|
.slice(0, 12)
|
|
|
|
const securityHeaders = [
|
|
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
// `microphone=(self)`: the composer's dictation button is ours.
|
|
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(self), geolocation=()' },
|
|
]
|
|
|
|
// The deep links the build declares (public/_fallback.json, exported beside the
|
|
// pages): a path under a declared prefix that no page answers renders that
|
|
// prefix's page, as the site edge (hanzo-inc/cloud apps/sites) and
|
|
// scripts/serve.mjs answer the same file. Dev reads it here so all three agree.
|
|
export function deepLinks(table = JSON.parse(readFileSync(new URL('./public/_fallback.json', import.meta.url), 'utf8'))) {
|
|
return Object.entries(table).map(([prefix, page]) => ({
|
|
source: `${prefix.replace(/\/+$/, '')}/:rest*`,
|
|
destination: `/${page.replace(/^\/+/, '').replace(/\.html?$/, '')}`,
|
|
}))
|
|
}
|
|
|
|
const nextConfig = {
|
|
output: 'export',
|
|
allowedDevOrigins: ['10.0.0.24', '10.0.0.*', 'localhost', '127.0.0.1'],
|
|
// An export is files, not a server: nothing reads the traces that cost 45 s.
|
|
outputFileTracing: false,
|
|
generateBuildId: async () => buildId,
|
|
// Types are checked once, by `pnpm typecheck`.
|
|
typescript: {
|
|
ignoreBuildErrors: true,
|
|
},
|
|
// Dev only: `output: 'export'` has no server, so the shipped site talks to
|
|
// api.hanzo.ai directly. Proxying keeps localhost reads same-origin, where
|
|
// CORS does not apply. Point HANZO_API_ORIGIN at the cloud host in .env.local
|
|
// and verify with a route only the host serves, such as /v1/templates.
|
|
async rewrites() {
|
|
const api = process.env.HANZO_API_ORIGIN || 'http://127.0.0.1:8080'
|
|
const s3 = process.env.HANZO_S3_ORIGIN || 'http://127.0.0.1:9005'
|
|
return {
|
|
afterFiles: [
|
|
{ source: '/pay/:path*', destination: 'https://hanzo.ai/pay/:path*' },
|
|
{ source: '/v1/s3/:path*', destination: `${s3}/v1/s3/:path*` },
|
|
{ source: '/v1/:path*', destination: `${api}/v1/:path*` },
|
|
{ source: '/mcp', destination: `${api}/mcp` },
|
|
{ source: '/mcp/:path*', destination: `${api}/mcp/:path*` },
|
|
{ source: '/graphql', destination: `${api}/graphql` },
|
|
],
|
|
// Only once no page and no file answered: a declared deep link.
|
|
fallback: deepLinks(),
|
|
}
|
|
},
|
|
// Next's dev badge lands on the org column.
|
|
devIndicators: false,
|
|
// TypeScript 7 has no compiler API; Next shells out to tsc instead.
|
|
experimental: {
|
|
useTypeScriptCli: true,
|
|
// @hanzo/ui imports ten brand icons from this barrel, and webpack compiled
|
|
// all 3,454 of them for both the server and the browser: 6,908 of the
|
|
// build's 18,972 modules. Rewritten to direct imports, the scripts the
|
|
// export ships are byte-identical.
|
|
optimizePackageImports: ['@icons-pack/react-simple-icons'],
|
|
},
|
|
poweredByHeader: false,
|
|
// The gui packages ship untranspiled ESM resolved against react-native.
|
|
//
|
|
// react-native-svg is here for its resolution, not its syntax. It ships a web
|
|
// build beside the native one (ReactNativeSVG.web.js, elements.web.js), chosen
|
|
// only by `.web` coming first among the extensions. Turbopack applies
|
|
// `resolveExtensions` to the project and these packages and never to the rest
|
|
// of node_modules, so outside this list it bundled the native build: Flow
|
|
// source from react-native and TurboModuleRegistry, which react-native-web
|
|
// does not export.
|
|
transpilePackages: ['@hanzo/gui', '@hanzo/ui', '@hanzo/data', '@hanzo/usage', '@hanzo/tabs', 'react-native-web', 'react-native-svg'],
|
|
turbopack: {
|
|
// Exact names: a subpath such as react-native/Libraries/... is not rewritten.
|
|
resolveAlias: {
|
|
'react-native': 'react-native-web',
|
|
// react-native-svg reads the asset registry; the web one lives here.
|
|
'@react-native/assets-registry/registry': 'react-native-web/dist/modules/AssetRegistry',
|
|
},
|
|
// The web build of a file first, then Turbopack's own list.
|
|
resolveExtensions: ['.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
|
|
},
|
|
images: {
|
|
unoptimized: true,
|
|
},
|
|
trailingSlash: false,
|
|
async headers() {
|
|
return [{ source: '/(.*)', headers: securityHeaders }]
|
|
},
|
|
}
|
|
|
|
export default nextConfig
|