-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathvite.config.ts
More file actions
167 lines (156 loc) · 5.46 KB
/
vite.config.ts
File metadata and controls
167 lines (156 loc) · 5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { createHash, randomBytes } from 'crypto'
import { readFileSync } from 'fs'
import { resolve } from 'path'
import tailwindcss from '@tailwindcss/vite'
import basicSsl from '@vitejs/plugin-basic-ssl'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { z } from 'zod/v4'
import vercelConfig from './vercel.json'
const ApiMode = z.enum(['msw', 'remote', 'nexus'])
function bail(msg: string): never {
console.error(msg)
process.exit(1)
}
const apiModeResult = ApiMode.default('nexus').safeParse(process.env.API_MODE)
if (!apiModeResult.success) {
const options = ApiMode.options.join(', ')
bail(`Error: API_MODE must be one of: [${options}]. If unset, default is "msw".`)
}
/**
* What API are we talking to? Only relevant in development mode.
*
* - `msw` (default): Mock Service Worker
* - `dogfood`: Dogfood rack at oxide.sys.rack2.eng.oxide.computer. Requires VPN.
* - `nexus`: Builds for production, assumes Nexus at localhost:12220 in dev mode only
*/
const apiMode = apiModeResult.data
if (apiMode === 'remote' && !process.env.EXT_HOST) {
bail(`Error: EXT_HOST is required when API_MODE=remote. See package.json for examples.`)
}
const EXT_HOST = process.env.EXT_HOST
const previewTags = [
{
injectTo: 'head' as const,
tag: 'script',
attrs: {
'data-domain':
process.env.VERCEL_ENV === 'production'
? 'oxide-console-preview.vercel.app'
: // not a real domain. we're only using it to distinguish prod
// from preview traffic in plausible
'console-pr-preview.vercel.app',
defer: true,
src: '/viewscript.js',
},
},
{
injectTo: 'head' as const,
tag: 'meta',
attrs: {
property: 'og:image',
content: '/assets/og-preview-image.webp',
},
},
{
injectTo: 'head' as const,
tag: 'meta',
attrs: {
property: 'og:description',
content: 'Preview of the Oxide web console with in-browser mock API',
},
},
]
// vercel config is source of truth for headers
const vercelHeaders = vercelConfig.headers[0].headers
const headers = Object.fromEntries(vercelHeaders.map((h) => [h.key, h.value]))
// This is only needed for local dev to avoid breaking Vite's script injection.
// Rather than use unsafe-inline all the time, the nonce approach is much more
// narrowly scoped and lets us make sure everything *else* works fine without
// unsafe-inline.
const cspNonce = randomBytes(8).toString('hex')
const csp = headers['content-security-policy']
const devHeaders = {
...headers,
'content-security-policy': `${csp}; script-src 'nonce-${cspNonce}' 'self'`,
}
// see https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
build: {
outDir: resolve(__dirname, 'dist'),
emptyOutDir: true,
sourcemap: true,
// minify: false, // uncomment for debugging
// prevent inlining assets as `data:`, which is not permitted by our Content-Security-Policy
assetsInlineLimit: 0,
},
define: {
'process.env.MSW': JSON.stringify(apiMode === 'msw'),
// we don't want to have to look at this banner all day
'process.env.MSW_BANNER': JSON.stringify(apiMode === 'msw' && mode === 'production'),
// used in production build to console.log the SHA at page load
'process.env.SHA': JSON.stringify(process.env.SHA),
// used by MSW — number for % likelihood of API request failure (decimals allowed)
'process.env.CHAOS': JSON.stringify(mode !== 'production' && process.env.CHAOS),
},
plugins: [
tailwindcss(),
{
name: 'inject-html-tags',
transformIndexHtml: () => (process.env.VERCEL ? previewTags : []),
},
{
// Inject theme-init.js as a classic (non-module) render-blocking script
// so it sets data-theme before first paint. It lives in public/assets/
// so it passes CSP default-src 'self' and is served by the /assets/*
// route in Nexus. We inject it here rather than putting it in index.html
// because Vite tries to bundle any <script src> it finds there. Content
// hash query param handles cache-busting since public/ files aren't
// fingerprinted by Vite.
name: 'theme-init',
transformIndexHtml() {
const content = readFileSync(resolve(__dirname, 'public/assets/theme-init.js'))
const hash = createHash('sha256').update(content).digest('hex').slice(0, 8)
return [
{
injectTo: 'head-prepend',
tag: 'script',
attrs: { src: `/assets/theme-init.js?v=${hash}` },
},
]
},
},
react(),
apiMode === 'remote' && basicSsl(),
],
html: {
// don't include a placeholder nonce in production.
// use a CSP nonce in dev to avoid needing to permit 'unsafe-inline'
cspNonce: mode === 'production' ? undefined : cspNonce,
},
server: {
port: 4000,
headers: devHeaders,
// these only get hit when MSW doesn't intercept the request
proxy: {
'/v1': {
target: apiMode === 'remote' ? `https://${EXT_HOST}` : 'http://localhost:12220',
changeOrigin: true,
},
},
},
resolve: { tsconfigPaths: true },
preview: { headers },
test: {
environment: 'jsdom',
setupFiles: ['test/unit/setup.ts'],
includeSource: ['app/**/*.ts'],
},
}))