import { copyFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig, type Plugin } from 'vite'; import { publishPlugins } from './tools/lib/plugins.mjs'; import { SITE_PATH } from './src/core/site'; const __dirname = dirname(fileURLToPath(import.meta.url)); /** * The workspace folder itself is what nginx serves under https://nockal.com/feuerwerk/. * The Vite source pages live in `web/`: `index.html` is the editor, `play.html` a released game * (#266). The build writes hashed assets into `dist/` and this plugin copies the built pages to the * folder root, so that the site URLs load the production bundles while `api/` and `data/` sit next * to them. */ const PAGES = ['index.html', 'play.html']; function publishPages(): Plugin { return { name: 'feuerwerk:publish-pages', apply: 'build', closeBundle() { for (const page of PAGES) copyFileSync(resolve(__dirname, 'dist', page), resolve(__dirname, page)); }, }; } /** * What the built pages may load (#427): their own files, Rapier's WebAssembly, images, sounds and * fonts from data and blob URLs, and Pixi's workers from blob URLs. No inline script or style, and * no `eval`: Pixi gets by without it through `pixi.js/unsafe-eval` (see src/page.ts). nginx sends * the same policy as a header; this copy is the one CI runs the whole suite under. Only the build * gets it, the dev server injects its CSS as inline styles. */ const CONTENT_SECURITY_POLICY = [ "default-src 'self'", "script-src 'self' 'wasm-unsafe-eval'", "style-src 'self'", "img-src 'self' data: blob:", "media-src 'self' data: blob:", "font-src 'self' data: blob:", "connect-src 'self' data: blob:", "worker-src 'self' blob:", "object-src 'none'", "base-uri 'none'", "form-action 'none'", ].join('; '); function contentSecurityPolicy(): Plugin { return { name: 'feuerwerk:content-security-policy', apply: 'build', transformIndexHtml: () => [ { tag: 'meta', attrs: { 'http-equiv': 'Content-Security-Policy', content: CONTENT_SECURITY_POLICY }, injectTo: 'head-prepend', }, ], }; } /** * Editor plugins are loaded from `data/plugins/`, which is not in the repository; the ones that * ship with it live in `plugins/` and are put there on every build. A file of the same name is * replaced, anything else in the folder is left alone. Whether one runs is the open project's * business, not the build's (#119). */ function shipPlugins(): Plugin { return { name: 'feuerwerk:publish-plugins', apply: 'build', closeBundle() { publishPlugins(__dirname); }, }; } export default defineConfig({ root: 'web', base: `${SITE_PATH}dist/`, publicDir: false, plugins: [contentSecurityPolicy(), publishPages(), shipPlugins()], build: { outDir: '../dist', emptyOutDir: true, target: 'es2022', sourcemap: true, rollupOptions: { input: Object.fromEntries( PAGES.map((page) => [page.replace('.html', ''), resolve(__dirname, 'web', page)]), ), }, }, server: { port: 5175, // During development the PHP API is served by the production nginx; proxy it. proxy: { [`${SITE_PATH}api`]: { target: 'https://nockal.com', changeOrigin: true, }, }, }, test: { root: '.', include: ['tests/**/*.test.ts'], environment: 'node', // Unset locally, so vitest keeps its own guess (one worker per core). CI sets it while the // gate runs next to the editor suite: the two jobs share four cores, and the suite measures // text widths and frame counts, which a machine under full load reports differently. maxWorkers: Number(process.env.GM_TEST_WORKERS) || undefined, }, });