import { readFileSync, writeFileSync } from 'node:fs'; import { basename, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig, type Plugin } from 'vite'; import { publishPlugins } from './tools/lib/plugins.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); /** * The workspace folder itself is what nginx serves under https://nockal.com/feuerwerk/, and the * same folder answers at https://feuerwerk.nockal.com/. So the build knows no path: its base is * relative, and the pages find the API and the data from where they are (src/core/site.ts). * 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. One folder up, their `./assets/` becomes `./dist/assets/`. */ const PAGES = ['index.html', 'play.html']; function publishPages(): Plugin { let outDir = ''; return { name: 'feuerwerk:publish-pages', apply: 'build', configResolved(config) { outDir = resolve(config.root, config.build.outDir); }, closeBundle() { const folder = basename(outDir); for (const page of PAGES) { const html = readFileSync(resolve(outDir, page), 'utf8'); const published = html.replaceAll('"./assets/', `"./${folder}/assets/`); if (published === html) throw new Error(`${page}: no ./assets/ link to move`); writeFileSync(resolve(outDir, '..', page), published); } }, }; } /** * 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: './', 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 and the data are served by the production nginx; proxy them. proxy: { '^/(api|data)/': { target: 'https://nockal.com', changeOrigin: true, rewrite: (path) => `/feuerwerk${path}`, }, }, }, 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, }, });