Files
feuerwerk/vite.config.ts
T
adminandClaude Opus 5.5 edef2b6323
feuerwerk / gate (push) Successful in 4m45s
feuerwerk / suite (push) Successful in 19m52s
Serve the editor from feuerwerk.nockal.com as well
The subdomain proxies to the same folder, but the build had the path
/feuerwerk/ baked in. On feuerwerk.nockal.com the page loaded its
assets, the API and the data URLs from /feuerwerk/... and got 404. The
session cookie had Path=/feuerwerk/, so the browser would never have
sent it to /api/ there either.

- site.ts takes the base path from the folder of the open page:
  /feuerwerk/ on nockal.com, / on the subdomain. Release links point at
  the address you are working on.
- vite.config.ts builds with a relative base. The copy plugin rewrites
  the pages to ./dist/assets/ and stops the build if it finds nothing
  to rewrite. The dev server now runs at http://localhost:5175/.
- Auth.php sets the cookie without a Path, so it belongs to the API
  folder the browser sees. Sign-in and sign-out also clear the old
  /feuerwerk/ cookie, otherwise it would keep signing in after a
  logout. Existing sessions stay valid.
- README: the new dev server address.

Tests:
- curl on both addresses: pages, JS, CSS and API 200
- sign-in, session, sign-out on both addresses
- e2e 70 against both addresses: 13/13 each, no CSP violation
- tsc, lint, prettier clean, site and releases tests 14/14
- PHP tests 7 of 8 files green, videos.test.php fails only because
  the web-php-fpm container has no ffmpeg

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:40:18 +02:00

128 lines
4.4 KiB
TypeScript

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,
},
});