|
| 1 | +import { defineConfig, mergeConfig } from 'vite' |
| 2 | +import { fileURLToPath } from 'node:url' |
| 3 | +import { existsSync } from 'node:fs' |
| 4 | +import path from 'node:path' |
| 5 | +import base from '../vite.config.ts' |
| 6 | + |
| 7 | +// QA-only vite config for running a dev server INSIDE a git worktree. |
| 8 | +// |
| 9 | +// A worktree has no node_modules of its own — deps resolve up to the main |
| 10 | +// checkout — so vite's default `server.fs.allow` (rooted at the worktree) |
| 11 | +// rejects every parent-dependency request with 403 and the app hangs on |
| 12 | +// "Loading…". |
| 13 | +// |
| 14 | +// Do NOT fix this by disabling `fs.strict`: that makes Vite serve ANY local |
| 15 | +// file over `/@fs/...` (e.g. /etc/hosts, ~/.ssh, .env), which is a real hazard |
| 16 | +// because this server is reached over the Tailscale tunnel during iPad testing |
| 17 | +// (VITE_TUNNEL in vite.config.ts), not just localhost. Instead keep strict on |
| 18 | +// and explicitly allow the worktree plus the checkout that actually holds |
| 19 | +// node_modules — Vite's default deny list still blocks everything outside. |
| 20 | +// |
| 21 | +// Kept as plain .mjs (like webkit-qa.mjs) so `tsc -b` doesn't typecheck it |
| 22 | +// against the app's vite config types. |
| 23 | +// |
| 24 | +// Run from the worktree root, pointing node DIRECTLY at the installed Vite |
| 25 | +// binary. The worktree has no node_modules/vite of its own (so the bare |
| 26 | +// `node node_modules/vite/bin/vite.js` exits with MODULE_NOT_FOUND), and Vite's |
| 27 | +// package `exports` don't expose the bin to require.resolve — so derive the |
| 28 | +// checkout that holds it from git. This form works from a worktree or a normal |
| 29 | +// checkout regardless of nesting depth: |
| 30 | +// |
| 31 | +// node "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")/node_modules/vite/bin/vite.js" \ |
| 32 | +// --config scripts/vite-qa.config.mjs --port 5199 --strictPort |
| 33 | +const worktreeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') |
| 34 | +// Walk up to the nearest ancestor whose node_modules holds REAL packages (the |
| 35 | +// main checkout, when run from a worktree; the worktree itself for a normal |
| 36 | +// checkout). A worktree's own node_modules is cache-only (.vite/.cache) — node |
| 37 | +// resolves actual deps up the tree — so probe for `node_modules/vite` (the dev |
| 38 | +// server we're running) rather than a bare node_modules dir. depsRoot contains |
| 39 | +// the worktree, so allowing it covers both source and the resolved deps. |
| 40 | +let depsRoot = worktreeRoot |
| 41 | +while ( |
| 42 | + !existsSync(path.join(depsRoot, 'node_modules', 'vite')) && |
| 43 | + path.dirname(depsRoot) !== depsRoot |
| 44 | +) { |
| 45 | + depsRoot = path.dirname(depsRoot) |
| 46 | +} |
| 47 | +// Fail CLOSED. If no ancestor has node_modules/vite, the loop above exits with |
| 48 | +// depsRoot at the filesystem root — allow-listing `/` would re-open the very |
| 49 | +// arbitrary-file-read hole this config exists to prevent. Refuse to start. |
| 50 | +if (!existsSync(path.join(depsRoot, 'node_modules', 'vite'))) { |
| 51 | + throw new Error( |
| 52 | + `vite-qa.config: no ancestor of ${worktreeRoot} has node_modules/vite — ` + |
| 53 | + `refusing to start rather than allow-list the filesystem root. ` + |
| 54 | + `Run from a worktree nested under an installed checkout.`, |
| 55 | + ) |
| 56 | +} |
| 57 | + |
| 58 | +export default defineConfig(async (env) => { |
| 59 | + const resolved = typeof base === 'function' ? await base(env) : base |
| 60 | + return mergeConfig(resolved, { server: { fs: { allow: [worktreeRoot, depsRoot] } } }) |
| 61 | +}) |
0 commit comments