diff options
Diffstat (limited to 'subprojects')
-rw-r--r-- | subprojects/frontend/.eslintrc.cjs | 8 | ||||
-rw-r--r-- | subprojects/frontend/config/backendConfigVitePlugin.ts | 27 | ||||
-rw-r--r-- | subprojects/frontend/config/detectDevModeOptions.ts | 94 | ||||
-rw-r--r-- | subprojects/frontend/config/fetchPackageMetadata.ts | 20 | ||||
-rw-r--r-- | subprojects/frontend/config/manifest.ts | 40 | ||||
-rw-r--r-- | subprojects/frontend/config/minifyHTMLVitePlugin.ts | 24 | ||||
-rw-r--r-- | subprojects/frontend/config/preloadFontsVitePlugin.ts | 24 | ||||
-rw-r--r-- | subprojects/frontend/package.json | 5 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/BackendConfig.ts | 13 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/fetchBackendConfig.ts | 12 | ||||
-rw-r--r-- | subprojects/frontend/tsconfig.json | 8 | ||||
-rw-r--r-- | subprojects/frontend/tsconfig.node.json | 4 | ||||
-rw-r--r-- | subprojects/frontend/tsconfig.shared.json | 13 | ||||
-rw-r--r-- | subprojects/frontend/vite.config.ts | 183 |
14 files changed, 310 insertions, 165 deletions
diff --git a/subprojects/frontend/.eslintrc.cjs b/subprojects/frontend/.eslintrc.cjs index eadd3fb4..8a7b474a 100644 --- a/subprojects/frontend/.eslintrc.cjs +++ b/subprojects/frontend/.eslintrc.cjs | |||
@@ -4,6 +4,7 @@ const path = require('node:path'); | |||
4 | const project = [ | 4 | const project = [ |
5 | path.join(__dirname, 'tsconfig.json'), | 5 | path.join(__dirname, 'tsconfig.json'), |
6 | path.join(__dirname, 'tsconfig.node.json'), | 6 | path.join(__dirname, 'tsconfig.node.json'), |
7 | path.join(__dirname, 'tsconfig.shared.json'), | ||
7 | ]; | 8 | ]; |
8 | 9 | ||
9 | /** @type {import('eslint').Linter.Config} */ | 10 | /** @type {import('eslint').Linter.Config} */ |
@@ -86,7 +87,12 @@ module.exports = { | |||
86 | }, | 87 | }, |
87 | }, | 88 | }, |
88 | { | 89 | { |
89 | files: ['.eslintrc.cjs', 'prettier.config.cjs', 'vite.config.ts'], | 90 | files: [ |
91 | '.eslintrc.cjs', | ||
92 | 'config/*.ts', | ||
93 | 'prettier.config.cjs', | ||
94 | 'vite.config.ts', | ||
95 | ], | ||
90 | env: { | 96 | env: { |
91 | browser: false, | 97 | browser: false, |
92 | node: true, | 98 | node: true, |
diff --git a/subprojects/frontend/config/backendConfigVitePlugin.ts b/subprojects/frontend/config/backendConfigVitePlugin.ts new file mode 100644 index 00000000..7a6bc3db --- /dev/null +++ b/subprojects/frontend/config/backendConfigVitePlugin.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import type { PluginOption } from 'vite'; | ||
2 | |||
3 | import type BackendConfig from '../src/xtext/BackendConfig'; | ||
4 | import { ENDPOINT } from '../src/xtext/BackendConfig'; | ||
5 | |||
6 | export default function backendConfigVitePlugin( | ||
7 | backendConfig: BackendConfig, | ||
8 | ): PluginOption { | ||
9 | return { | ||
10 | name: 'backend-config', | ||
11 | apply: 'serve', | ||
12 | configureServer(server) { | ||
13 | const config = JSON.stringify(backendConfig); | ||
14 | server.middlewares.use((req, res, next) => { | ||
15 | if (req.url === `/${ENDPOINT}`) { | ||
16 | res.setHeader('Content-Type', 'application/json'); | ||
17 | res.end(config); | ||
18 | } else { | ||
19 | next(); | ||
20 | } | ||
21 | }); | ||
22 | }, | ||
23 | }; | ||
24 | } | ||
25 | |||
26 | export type { default as BackendConfig } from '../src/xtext/BackendConfig'; | ||
27 | export { ENDPOINT as CONFIG_ENDPOINT } from '../src/xtext/BackendConfig'; | ||
diff --git a/subprojects/frontend/config/detectDevModeOptions.ts b/subprojects/frontend/config/detectDevModeOptions.ts new file mode 100644 index 00000000..b3696241 --- /dev/null +++ b/subprojects/frontend/config/detectDevModeOptions.ts | |||
@@ -0,0 +1,94 @@ | |||
1 | import type { PluginOption, ServerOptions } from 'vite'; | ||
2 | |||
3 | import backendConfigVitePlugin, { | ||
4 | type BackendConfig, | ||
5 | } from './backendConfigVitePlugin'; | ||
6 | |||
7 | export const API_ENDPOINT = 'xtext-service'; | ||
8 | |||
9 | export interface DevModeOptions { | ||
10 | mode: string; | ||
11 | isDevelopment: boolean; | ||
12 | devModePlugins: PluginOption[]; | ||
13 | serverOptions: ServerOptions; | ||
14 | } | ||
15 | |||
16 | interface ListenOptions { | ||
17 | host: string; | ||
18 | port: number; | ||
19 | secure: boolean; | ||
20 | } | ||
21 | |||
22 | function detectListenOptions( | ||
23 | name: string, | ||
24 | fallbackHost: string, | ||
25 | fallbackPort: number, | ||
26 | ): ListenOptions { | ||
27 | const host = process.env[`${name}_HOST`] ?? fallbackHost; | ||
28 | const rawPort = process.env[`${name}_PORT`]; | ||
29 | const port = rawPort === undefined ? fallbackPort : parseInt(rawPort, 10); | ||
30 | const secure = port === 443; | ||
31 | return { host, port, secure }; | ||
32 | } | ||
33 | |||
34 | function listenURL( | ||
35 | { host, port, secure }: ListenOptions, | ||
36 | protocol = 'http', | ||
37 | ): string { | ||
38 | return `${secure ? `${protocol}s` : protocol}://${host}:${port}`; | ||
39 | } | ||
40 | |||
41 | export default function detectDevModeOptions(): DevModeOptions { | ||
42 | const mode = process.env['MODE'] || 'development'; | ||
43 | const isDevelopment = mode === 'development'; | ||
44 | |||
45 | if (!isDevelopment) { | ||
46 | return { | ||
47 | mode, | ||
48 | isDevelopment, | ||
49 | devModePlugins: [], | ||
50 | serverOptions: {}, | ||
51 | }; | ||
52 | } | ||
53 | |||
54 | const listen = detectListenOptions('LISTEN', 'localhost', 1313); | ||
55 | // Make sure we always use IPv4 to connect to the backend, | ||
56 | // because it doesn't listen on IPv6. | ||
57 | const api = detectListenOptions('API', '127.0.0.1', 1312); | ||
58 | const publicAddress = detectListenOptions('PUBLIC', listen.host, listen.port); | ||
59 | |||
60 | const backendConfig: BackendConfig = { | ||
61 | webSocketURL: `${listenURL(publicAddress, 'ws')}/${API_ENDPOINT}`, | ||
62 | }; | ||
63 | |||
64 | return { | ||
65 | mode, | ||
66 | isDevelopment, | ||
67 | devModePlugins: [backendConfigVitePlugin(backendConfig)], | ||
68 | serverOptions: { | ||
69 | host: listen.host, | ||
70 | port: listen.port, | ||
71 | strictPort: true, | ||
72 | https: listen.secure, | ||
73 | headers: { | ||
74 | // Enable strict origin isolation, see e.g., | ||
75 | // https://github.com/vitejs/vite/issues/3909#issuecomment-1065893956 | ||
76 | 'Cross-Origin-Opener-Policy': 'same-origin', | ||
77 | 'Cross-Origin-Embedder-Policy': 'require-corp', | ||
78 | 'Cross-Origin-Resource-Policy': 'cross-origin', | ||
79 | }, | ||
80 | proxy: { | ||
81 | [`/${API_ENDPOINT}`]: { | ||
82 | target: listenURL(api), | ||
83 | ws: true, | ||
84 | secure: api.secure, | ||
85 | }, | ||
86 | }, | ||
87 | hmr: { | ||
88 | host: publicAddress.host, | ||
89 | clientPort: publicAddress.port, | ||
90 | path: '/vite', | ||
91 | }, | ||
92 | }, | ||
93 | }; | ||
94 | } | ||
diff --git a/subprojects/frontend/config/fetchPackageMetadata.ts b/subprojects/frontend/config/fetchPackageMetadata.ts new file mode 100644 index 00000000..50807b03 --- /dev/null +++ b/subprojects/frontend/config/fetchPackageMetadata.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import { readFile } from 'node:fs/promises'; | ||
2 | import path from 'node:path'; | ||
3 | |||
4 | import z from 'zod'; | ||
5 | |||
6 | const PackageInfo = z.object({ | ||
7 | name: z.string().min(1), | ||
8 | version: z.string().min(1), | ||
9 | }); | ||
10 | |||
11 | export default async function fetchPackageMetadata( | ||
12 | thisDir: string, | ||
13 | ): Promise<void> { | ||
14 | const contents = await readFile(path.join(thisDir, 'package.json'), 'utf8'); | ||
15 | const { name: packageName, version: packageVersion } = PackageInfo.parse( | ||
16 | JSON.parse(contents), | ||
17 | ); | ||
18 | process.env['VITE_PACKAGE_NAME'] ??= packageName; | ||
19 | process.env['VITE_PACKAGE_VERSION'] ??= packageVersion; | ||
20 | } | ||
diff --git a/subprojects/frontend/config/manifest.ts b/subprojects/frontend/config/manifest.ts new file mode 100644 index 00000000..3cec777c --- /dev/null +++ b/subprojects/frontend/config/manifest.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import type { ManifestOptions } from 'vite-plugin-pwa'; | ||
2 | |||
3 | const manifest: Partial<ManifestOptions> = { | ||
4 | lang: 'en-US', | ||
5 | name: 'Refinery', | ||
6 | short_name: 'Refinery', | ||
7 | description: 'An efficient graph sovler for generating well-formed models', | ||
8 | theme_color: '#f5f5f5', | ||
9 | display_override: ['window-controls-overlay'], | ||
10 | display: 'standalone', | ||
11 | background_color: '#21252b', | ||
12 | icons: [ | ||
13 | { | ||
14 | src: 'icon-192x192.png', | ||
15 | sizes: '192x192', | ||
16 | type: 'image/png', | ||
17 | purpose: 'any maskable', | ||
18 | }, | ||
19 | { | ||
20 | src: 'icon-512x512.png', | ||
21 | sizes: '512x512', | ||
22 | type: 'image/png', | ||
23 | purpose: 'any maskable', | ||
24 | }, | ||
25 | { | ||
26 | src: 'icon-any.svg', | ||
27 | sizes: 'any', | ||
28 | type: 'image/svg+xml', | ||
29 | purpose: 'any maskable', | ||
30 | }, | ||
31 | { | ||
32 | src: 'mask-icon.svg', | ||
33 | sizes: 'any', | ||
34 | type: 'image/svg+xml', | ||
35 | purpose: 'monochrome', | ||
36 | }, | ||
37 | ], | ||
38 | }; | ||
39 | |||
40 | export default manifest; | ||
diff --git a/subprojects/frontend/config/minifyHTMLVitePlugin.ts b/subprojects/frontend/config/minifyHTMLVitePlugin.ts new file mode 100644 index 00000000..18336d4d --- /dev/null +++ b/subprojects/frontend/config/minifyHTMLVitePlugin.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { minify, type Options as TerserOptions } from 'html-minifier-terser'; | ||
2 | import type { PluginOption } from 'vite'; | ||
3 | |||
4 | export default function minifyHTMLVitePlugin( | ||
5 | options?: TerserOptions | undefined, | ||
6 | ): PluginOption { | ||
7 | return { | ||
8 | name: 'minify-html', | ||
9 | apply: 'build', | ||
10 | enforce: 'post', | ||
11 | transformIndexHtml(html) { | ||
12 | return minify(html, { | ||
13 | collapseWhitespace: true, | ||
14 | collapseBooleanAttributes: true, | ||
15 | minifyCSS: true, | ||
16 | removeComments: true, | ||
17 | removeAttributeQuotes: true, | ||
18 | removeRedundantAttributes: true, | ||
19 | sortAttributes: true, | ||
20 | ...(options ?? {}), | ||
21 | }); | ||
22 | }, | ||
23 | }; | ||
24 | } | ||
diff --git a/subprojects/frontend/config/preloadFontsVitePlugin.ts b/subprojects/frontend/config/preloadFontsVitePlugin.ts new file mode 100644 index 00000000..bc6f8eaf --- /dev/null +++ b/subprojects/frontend/config/preloadFontsVitePlugin.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import micromatch from 'micromatch'; | ||
2 | import type { PluginOption } from 'vite'; | ||
3 | |||
4 | export default function preloadFontsVitePlugin( | ||
5 | fontsGlob: string | string[], | ||
6 | ): PluginOption { | ||
7 | return { | ||
8 | name: 'refinery-preload-fonts', | ||
9 | apply: 'build', | ||
10 | enforce: 'post', | ||
11 | transformIndexHtml(_html, { bundle }) { | ||
12 | return micromatch(Object.keys(bundle ?? {}), fontsGlob).map((href) => ({ | ||
13 | tag: 'link', | ||
14 | attrs: { | ||
15 | href, | ||
16 | rel: 'preload', | ||
17 | type: 'font/woff2', | ||
18 | as: 'font', | ||
19 | crossorigin: 'anonymous', | ||
20 | }, | ||
21 | })); | ||
22 | }, | ||
23 | }; | ||
24 | } | ||
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index db5d3f68..a826755d 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json | |||
@@ -7,7 +7,7 @@ | |||
7 | "build": "cross-env MODE=production vite build", | 7 | "build": "cross-env MODE=production vite build", |
8 | "serve": "cross-env MODE=development vite serve", | 8 | "serve": "cross-env MODE=development vite serve", |
9 | "typegen": "xstate typegen \"src/**/*.ts?(x)\"", | 9 | "typegen": "xstate typegen \"src/**/*.ts?(x)\"", |
10 | "typecheck": "tsc -p tsconfig.node.json && tsc -p tsconfig.json", | 10 | "typecheck": "tsc -p tsconfig.shared.json && tsc -p tsconfig.node.json && tsc -p tsconfig.json", |
11 | "lint": "eslint .", | 11 | "lint": "eslint .", |
12 | "lint:ci": "eslint -f json -o build/eslint.json .", | 12 | "lint:ci": "eslint -f json -o build/eslint.json .", |
13 | "lint:fix": "yarn run lint --fix" | 13 | "lint:fix": "yarn run lint --fix" |
@@ -62,6 +62,7 @@ | |||
62 | "@types/eslint": "^8.4.10", | 62 | "@types/eslint": "^8.4.10", |
63 | "@types/html-minifier-terser": "^7.0.0", | 63 | "@types/html-minifier-terser": "^7.0.0", |
64 | "@types/lodash-es": "^4.17.6", | 64 | "@types/lodash-es": "^4.17.6", |
65 | "@types/micromatch": "^4.0.2", | ||
65 | "@types/ms": "^0.7.31", | 66 | "@types/ms": "^0.7.31", |
66 | "@types/node": "^18.11.13", | 67 | "@types/node": "^18.11.13", |
67 | "@types/prettier": "^2.7.1", | 68 | "@types/prettier": "^2.7.1", |
@@ -83,10 +84,10 @@ | |||
83 | "eslint-plugin-react": "^7.31.11", | 84 | "eslint-plugin-react": "^7.31.11", |
84 | "eslint-plugin-react-hooks": "^4.6.0", | 85 | "eslint-plugin-react-hooks": "^4.6.0", |
85 | "html-minifier-terser": "^7.1.0", | 86 | "html-minifier-terser": "^7.1.0", |
87 | "micromatch": "^4.0.5", | ||
86 | "prettier": "^2.8.1", | 88 | "prettier": "^2.8.1", |
87 | "typescript": "4.9.3", | 89 | "typescript": "4.9.3", |
88 | "vite": "^4.0.0", | 90 | "vite": "^4.0.0", |
89 | "vite-plugin-inject-preload": "^1.1.1", | ||
90 | "vite-plugin-pwa": "^0.13.3", | 91 | "vite-plugin-pwa": "^0.13.3", |
91 | "workbox-window": "^6.5.4" | 92 | "workbox-window": "^6.5.4" |
92 | } | 93 | } |
diff --git a/subprojects/frontend/src/xtext/BackendConfig.ts b/subprojects/frontend/src/xtext/BackendConfig.ts new file mode 100644 index 00000000..41737c0b --- /dev/null +++ b/subprojects/frontend/src/xtext/BackendConfig.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */ | ||
2 | |||
3 | import { z } from 'zod'; | ||
4 | |||
5 | export const ENDPOINT = 'config.json'; | ||
6 | |||
7 | const BackendConfig = z.object({ | ||
8 | webSocketURL: z.string().url(), | ||
9 | }); | ||
10 | |||
11 | type BackendConfig = z.infer<typeof BackendConfig>; | ||
12 | |||
13 | export default BackendConfig; | ||
diff --git a/subprojects/frontend/src/xtext/fetchBackendConfig.ts b/subprojects/frontend/src/xtext/fetchBackendConfig.ts index f8087a70..15e976d8 100644 --- a/subprojects/frontend/src/xtext/fetchBackendConfig.ts +++ b/subprojects/frontend/src/xtext/fetchBackendConfig.ts | |||
@@ -1,15 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */ | 1 | import BackendConfig, { ENDPOINT } from './BackendConfig'; |
2 | |||
3 | import { z } from 'zod'; | ||
4 | |||
5 | export const BackendConfig = z.object({ | ||
6 | webSocketURL: z.string().url(), | ||
7 | }); | ||
8 | |||
9 | export type BackendConfig = z.infer<typeof BackendConfig>; | ||
10 | 2 | ||
11 | export default async function fetchBackendConfig(): Promise<BackendConfig> { | 3 | export default async function fetchBackendConfig(): Promise<BackendConfig> { |
12 | const configURL = `${import.meta.env.BASE_URL}config.json`; | 4 | const configURL = `${import.meta.env.BASE_URL}${ENDPOINT}`; |
13 | const response = await fetch(configURL); | 5 | const response = await fetch(configURL); |
14 | const rawConfig = (await response.json()) as unknown; | 6 | const rawConfig = (await response.json()) as unknown; |
15 | return BackendConfig.parse(rawConfig); | 7 | return BackendConfig.parse(rawConfig); |
diff --git a/subprojects/frontend/tsconfig.json b/subprojects/frontend/tsconfig.json index 0dccec40..35abd789 100644 --- a/subprojects/frontend/tsconfig.json +++ b/subprojects/frontend/tsconfig.json | |||
@@ -10,8 +10,12 @@ | |||
10 | "src", | 10 | "src", |
11 | "types" | 11 | "types" |
12 | ], | 12 | ], |
13 | "exclude": ["types/node"], | 13 | "exclude": [ |
14 | "src/xtext/BackendConfig.ts", | ||
15 | "types/node" | ||
16 | ], | ||
14 | "references": [ | 17 | "references": [ |
15 | { "path": "./tsconfig.node.json" } | 18 | { "path": "./tsconfig.node.json" }, |
19 | { "path": "./tsconfig.shared.json" } | ||
16 | ] | 20 | ] |
17 | } | 21 | } |
diff --git a/subprojects/frontend/tsconfig.node.json b/subprojects/frontend/tsconfig.node.json index c4539dbc..f4908bcb 100644 --- a/subprojects/frontend/tsconfig.node.json +++ b/subprojects/frontend/tsconfig.node.json | |||
@@ -9,8 +9,12 @@ | |||
9 | }, | 9 | }, |
10 | "include": [ | 10 | "include": [ |
11 | ".eslintrc.cjs", | 11 | ".eslintrc.cjs", |
12 | "config/*.ts", | ||
12 | "prettier.config.cjs", | 13 | "prettier.config.cjs", |
13 | "types/node", | 14 | "types/node", |
14 | "vite.config.ts" | 15 | "vite.config.ts" |
16 | ], | ||
17 | "references": [ | ||
18 | { "path": "./tsconfig.shared.json" } | ||
15 | ] | 19 | ] |
16 | } | 20 | } |
diff --git a/subprojects/frontend/tsconfig.shared.json b/subprojects/frontend/tsconfig.shared.json new file mode 100644 index 00000000..b7e1de55 --- /dev/null +++ b/subprojects/frontend/tsconfig.shared.json | |||
@@ -0,0 +1,13 @@ | |||
1 | { | ||
2 | "extends": "./tsconfig.base.json", | ||
3 | "compilerOptions": { | ||
4 | "composite": true, | ||
5 | "lib": ["ES2022"], | ||
6 | "types": [], | ||
7 | "emitDeclarationOnly": true, | ||
8 | "outDir": "build/typescript" | ||
9 | }, | ||
10 | "include": [ | ||
11 | "src/xtext/BackendConfig.ts", | ||
12 | ] | ||
13 | } | ||
diff --git a/subprojects/frontend/vite.config.ts b/subprojects/frontend/vite.config.ts index 08b3db0c..cd9993cc 100644 --- a/subprojects/frontend/vite.config.ts +++ b/subprojects/frontend/vite.config.ts | |||
@@ -1,160 +1,61 @@ | |||
1 | import { setDefaultResultOrder } from 'node:dns'; | ||
2 | import { readFileSync } from 'node:fs'; | ||
3 | import path from 'node:path'; | 1 | import path from 'node:path'; |
4 | import { fileURLToPath } from 'node:url'; | 2 | import { fileURLToPath } from 'node:url'; |
5 | 3 | ||
6 | import { lezer } from '@lezer/generator/rollup'; | 4 | import { lezer } from '@lezer/generator/rollup'; |
7 | import react from '@vitejs/plugin-react-swc'; | 5 | import react from '@vitejs/plugin-react-swc'; |
8 | import { minify } from 'html-minifier-terser'; | 6 | import { defineConfig, type UserConfig as ViteConfig } from 'vite'; |
9 | import { defineConfig, type PluginOption } from 'vite'; | ||
10 | import injectPreload from 'vite-plugin-inject-preload'; | ||
11 | import { VitePWA } from 'vite-plugin-pwa'; | 7 | import { VitePWA } from 'vite-plugin-pwa'; |
12 | 8 | ||
13 | setDefaultResultOrder('verbatim'); | 9 | import { CONFIG_ENDPOINT } from './config/backendConfigVitePlugin'; |
10 | import detectDevModeOptions, { | ||
11 | API_ENDPOINT, | ||
12 | } from './config/detectDevModeOptions'; | ||
13 | import fetchPackageMetadata from './config/fetchPackageMetadata'; | ||
14 | import manifest from './config/manifest'; | ||
15 | import minifyHTMLVitePlugin from './config/minifyHTMLVitePlugin'; | ||
16 | import preloadFontsVitePlugin from './config/preloadFontsVitePlugin'; | ||
14 | 17 | ||
15 | const thisDir = path.dirname(fileURLToPath(import.meta.url)); | 18 | const thisDir = path.dirname(fileURLToPath(import.meta.url)); |
16 | 19 | ||
17 | const mode = process.env['MODE'] || 'development'; | 20 | const { mode, isDevelopment, devModePlugins, serverOptions } = |
18 | const isDevelopment = mode === 'development'; | 21 | detectDevModeOptions(); |
19 | process.env['NODE_ENV'] ??= mode; | ||
20 | |||
21 | function portNumberOrElse(envName: string, fallback: number): number { | ||
22 | const value = process.env[envName]; | ||
23 | return value ? parseInt(value, 10) : fallback; | ||
24 | } | ||
25 | 22 | ||
26 | const listenHost = process.env['LISTEN_HOST'] || 'localhost'; | 23 | process.env['NODE_ENV'] ??= mode; |
27 | const listenPort = portNumberOrElse('LISTEN_PORT', 1313); | ||
28 | const apiHost = process.env['API_HOST'] || '127.0.0.1'; | ||
29 | const apiPort = portNumberOrElse('API_PORT', 1312); | ||
30 | const apiSecure = apiPort === 443; | ||
31 | const publicHost = process.env['PUBLIC_HOST'] || listenHost; | ||
32 | const publicPort = portNumberOrElse('PUBLIC_PORT', listenPort); | ||
33 | const publicSecure = publicPort === 443; | ||
34 | 24 | ||
35 | const { name: packageName, version: packageVersion } = JSON.parse( | 25 | const fontsGlob = [ |
36 | readFileSync(path.join(thisDir, 'package.json'), 'utf8'), | 26 | 'inter-latin-variable-wghtOnly-normal-*.woff2', |
37 | ) as { name: string; version: string }; | 27 | 'jetbrains-mono-latin-variable-wghtOnly-{normal,italic}-*.woff2', |
38 | process.env['VITE_PACKAGE_NAME'] ??= packageName; | 28 | ]; |
39 | process.env['VITE_PACKAGE_VERSION'] ??= packageVersion; | ||
40 | 29 | ||
41 | const minifyPlugin: PluginOption = { | 30 | const viteConfig: ViteConfig = { |
42 | name: 'minify-html', | ||
43 | enforce: 'post', | ||
44 | async transformIndexHtml(html) { | ||
45 | if (isDevelopment) { | ||
46 | return html; | ||
47 | } | ||
48 | return minify(html, { | ||
49 | collapseWhitespace: true, | ||
50 | collapseBooleanAttributes: true, | ||
51 | minifyCSS: true, | ||
52 | removeComments: true, | ||
53 | removeAttributeQuotes: true, | ||
54 | removeRedundantAttributes: true, | ||
55 | sortAttributes: true, | ||
56 | }); | ||
57 | }, | ||
58 | }; | ||
59 | |||
60 | const backendConfigPlugin: PluginOption = { | ||
61 | name: 'backend-config', | ||
62 | configureServer(server) { | ||
63 | const protocol = publicSecure ? 'wss' : 'ws'; | ||
64 | const webSocketURL = `${protocol}://${publicHost}:${publicPort}/xtext-service`; | ||
65 | const config = JSON.stringify({ webSocketURL }); | ||
66 | server.middlewares.use((req, res, next) => { | ||
67 | if (req.url === '/config.json') { | ||
68 | res.setHeader('Content-Type', 'application/json'); | ||
69 | res.end(config); | ||
70 | } else { | ||
71 | next(); | ||
72 | } | ||
73 | }); | ||
74 | }, | ||
75 | }; | ||
76 | |||
77 | export default defineConfig({ | ||
78 | logLevel: 'info', | 31 | logLevel: 'info', |
79 | mode, | 32 | mode, |
80 | root: thisDir, | 33 | root: thisDir, |
81 | cacheDir: path.join(thisDir, 'build/vite/cache'), | 34 | cacheDir: path.join(thisDir, 'build/vite/cache'), |
82 | plugins: [ | 35 | plugins: [ |
83 | minifyPlugin, | ||
84 | backendConfigPlugin, | ||
85 | react(), | 36 | react(), |
86 | injectPreload({ | ||
87 | files: [ | ||
88 | { | ||
89 | match: | ||
90 | /(?:inter-latin-variable-wghtOnly-normal|jetbrains-mono-latin-variable-wghtOnly-(?:italic|normal)).+\.woff2$/, | ||
91 | attributes: { | ||
92 | type: 'font/woff2', | ||
93 | as: 'font', | ||
94 | crossorigin: 'anonymous', | ||
95 | }, | ||
96 | }, | ||
97 | ], | ||
98 | }), | ||
99 | lezer(), | 37 | lezer(), |
38 | preloadFontsVitePlugin(fontsGlob), | ||
39 | minifyHTMLVitePlugin(), | ||
100 | VitePWA({ | 40 | VitePWA({ |
101 | strategies: 'generateSW', | 41 | strategies: 'generateSW', |
102 | registerType: 'prompt', | 42 | registerType: 'prompt', |
103 | injectRegister: null, | 43 | injectRegister: null, |
104 | workbox: { | 44 | workbox: { |
105 | globPatterns: [ | 45 | globPatterns: ['**/*.{css,html,js}', ...fontsGlob], |
106 | '**/*.{css,html,js}', | ||
107 | 'inter-latin-variable-wghtOnly-normal-*.woff2', | ||
108 | 'jetbrains-mono-latin-variable-wghtOnly-{normal,italic}-*.woff2', | ||
109 | ], | ||
110 | dontCacheBustURLsMatching: /\.(?:css|js|woff2?)$/, | 46 | dontCacheBustURLsMatching: /\.(?:css|js|woff2?)$/, |
111 | navigateFallbackDenylist: [/^\/xtext-service/], | 47 | navigateFallbackDenylist: [new RegExp(`^\\/${API_ENDPOINT}$`)], |
112 | runtimeCaching: [ | 48 | runtimeCaching: [ |
113 | { | 49 | { |
114 | urlPattern: 'config.json', | 50 | urlPattern: CONFIG_ENDPOINT, |
115 | handler: 'StaleWhileRevalidate', | 51 | handler: 'StaleWhileRevalidate', |
116 | }, | 52 | }, |
117 | ], | 53 | ], |
118 | }, | 54 | }, |
119 | includeAssets: ['apple-touch-icon.png', 'favicon.svg', 'mask-icon.svg'], | 55 | includeAssets: ['apple-touch-icon.png', 'favicon.svg'], |
120 | manifest: { | 56 | manifest, |
121 | lang: 'en-US', | ||
122 | name: 'Refinery', | ||
123 | short_name: 'Refinery', | ||
124 | description: | ||
125 | 'An efficient graph sovler for generating well-formed models', | ||
126 | theme_color: '#f5f5f5', | ||
127 | display_override: ['window-controls-overlay'], | ||
128 | display: 'standalone', | ||
129 | background_color: '#21252b', | ||
130 | icons: [ | ||
131 | { | ||
132 | src: 'icon-192x192.png', | ||
133 | sizes: '192x192', | ||
134 | type: 'image/png', | ||
135 | purpose: 'any maskable', | ||
136 | }, | ||
137 | { | ||
138 | src: 'icon-512x512.png', | ||
139 | sizes: '512x512', | ||
140 | type: 'image/png', | ||
141 | purpose: 'any maskable', | ||
142 | }, | ||
143 | { | ||
144 | src: 'icon-any.svg', | ||
145 | sizes: 'any', | ||
146 | type: 'image/svg+xml', | ||
147 | purpose: 'any maskable', | ||
148 | }, | ||
149 | { | ||
150 | src: 'mask-icon.svg', | ||
151 | sizes: 'any', | ||
152 | type: 'image/svg+xml', | ||
153 | purpose: 'monochrome', | ||
154 | }, | ||
155 | ], | ||
156 | }, | ||
157 | }), | 57 | }), |
58 | ...devModePlugins, | ||
158 | ], | 59 | ], |
159 | base: '', | 60 | base: '', |
160 | define: { | 61 | define: { |
@@ -162,36 +63,18 @@ export default defineConfig({ | |||
162 | }, | 63 | }, |
163 | build: { | 64 | build: { |
164 | assetsDir: '.', | 65 | assetsDir: '.', |
165 | // If we don't control inlining manually, | 66 | // If we don't control inlining manually, web fonts will be randomly inlined |
166 | // web fonts will randomly get inlined into the CSS, degrading performance. | 67 | // into the CSS, which degrades performance. |
167 | assetsInlineLimit: 0, | 68 | assetsInlineLimit: 0, |
168 | outDir: path.join('build/vite', mode), | 69 | outDir: path.join('build/vite', mode), |
169 | emptyOutDir: true, | 70 | emptyOutDir: true, |
170 | sourcemap: isDevelopment, | 71 | sourcemap: isDevelopment, |
171 | minify: !isDevelopment, | 72 | minify: !isDevelopment, |
172 | }, | 73 | }, |
173 | server: { | 74 | server: serverOptions, |
174 | host: listenHost, | 75 | }; |
175 | port: listenPort, | 76 | |
176 | strictPort: true, | 77 | export default defineConfig(async () => { |
177 | headers: { | 78 | await fetchPackageMetadata(thisDir); |
178 | // Enable strict origin isolation, see e.g., | 79 | return viteConfig; |
179 | // https://github.com/vitejs/vite/issues/3909#issuecomment-1065893956 | ||
180 | 'Cross-Origin-Opener-Policy': 'same-origin', | ||
181 | 'Cross-Origin-Embedder-Policy': 'require-corp', | ||
182 | 'Cross-Origin-Resource-Policy': 'cross-origin', | ||
183 | }, | ||
184 | proxy: { | ||
185 | '/xtext-service': { | ||
186 | target: `${apiSecure ? 'https' : 'http'}://${apiHost}:${apiPort}`, | ||
187 | ws: true, | ||
188 | secure: apiSecure, | ||
189 | }, | ||
190 | }, | ||
191 | hmr: { | ||
192 | host: publicHost, | ||
193 | clientPort: publicPort, | ||
194 | path: '/vite', | ||
195 | }, | ||
196 | }, | ||
197 | }); | 80 | }); |