Files
LivingDexTracker/scripts/run-with-local-supabase.mjs
T
2026-09-14 11:47:58 +01:00

123 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
const [, , command, ...args] = process.argv;
if (!command) {
console.error('Usage: node scripts/run-with-local-supabase.mjs <command> [...args]');
process.exit(2);
}
const SETUP_HINT = 'Run "npm run supabase:start" followed by "npm run supabase:reset".';
// npx is a shell script on Windows, where spawn needs a shell to find it.
const useShell = process.platform === 'win32';
function fail(message, detail) {
console.error(message);
if (detail) console.error(String(detail).trim());
process.exit(1);
}
const status = spawnSync('npx', ['supabase', 'status', '--output', 'json'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
shell: useShell
});
if (status.error) {
fail(
'Unable to run "npx supabase status" - is the supabase CLI installed?',
status.error.message
);
}
if (status.status !== 0) {
const stderr = status.stderr ?? '';
// Distinguish a stopped stack from a genuinely broken CLI invocation, so the hint is only
// printed when it is actually the advice the reader needs.
if (/not running|supabase start/i.test(stderr)) {
fail(`Local Supabase is required but is not running.\n${SETUP_HINT}`, stderr);
}
fail(`"supabase status" failed with exit code ${status.status}.`, stderr);
}
let values;
try {
values = JSON.parse(status.stdout);
} catch (error) {
fail('Unable to parse "supabase status --output json".', error);
}
const apiUrl = values.API_URL ?? values.api_url ?? 'http://127.0.0.1:54321';
const anonKey = values.ANON_KEY ?? values.PUBLISHABLE_KEY ?? values.anon_key;
const serviceRoleKey = values.SERVICE_ROLE_KEY ?? values.SECRET_KEY ?? values.service_role_key;
if (!anonKey || !serviceRoleKey) {
fail(`Supabase status did not return an anonymous and service-role key.\n${SETUP_HINT}`);
}
function isLoopbackUrl(value) {
try {
return ['127.0.0.1', 'localhost', '[::1]'].includes(new URL(value).hostname);
} catch {
return false;
}
}
if (!isLoopbackUrl(apiUrl)) {
fail(`Refusing to run local-stack tests against non-loopback Supabase URL: ${apiUrl}`);
}
const providerUrlVariables = [
'MOCK_PROVIDER_URL',
'GOOGLE_OAUTH_AUTHORIZE_URL',
'GOOGLE_OAUTH_TOKEN_URL',
'GOOGLE_DRIVE_API_URL',
'GOOGLE_DRIVE_UPLOAD_URL',
'DROPBOX_OAUTH_AUTHORIZE_URL',
'DROPBOX_OAUTH_TOKEN_URL',
'DROPBOX_UPLOAD_URL'
];
for (const name of providerUrlVariables) {
const value = process.env[name];
if (value && !isLoopbackUrl(value)) {
fail(`Refusing to run provider tests with non-loopback ${name}: ${value}`);
}
}
// A running-but-unseeded database is the most common broken state, and it surfaces downstream as
// a confusing assertion failure. Check it here instead.
const probe = await fetch(`${apiUrl}/rest/v1/pokedex_entries?select=id&limit=1`, {
headers: { apikey: anonKey, Authorization: `Bearer ${anonKey}` }
}).catch((error) => {
fail(`Unable to reach the local Supabase REST API at ${apiUrl}.\n${SETUP_HINT}`, error);
});
if (!probe.ok) {
fail(
`The local Supabase database has no readable pokedex_entries (HTTP ${probe.status}).\n${SETUP_HINT}`,
await probe.text()
);
}
if (((await probe.json()) ?? []).length === 0) {
fail(`The local Supabase database is empty - migrations or seeds have not run.\n${SETUP_HINT}`);
}
const child = spawnSync(command, args, {
stdio: 'inherit',
shell: useShell,
env: {
...process.env,
// Never allow an exported production value to redirect a local integration run.
PUBLIC_SUPABASE_URL: apiUrl,
PUBLIC_SUPABASE_ANON_KEY: anonKey,
SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
TEST_SUPABASE_URL: apiUrl,
TEST_SUPABASE_ANON_KEY: anonKey,
E2E_SERVICE_ROLE_KEY: serviceRoleKey
}
});
if (child.error) fail(`Unable to run "${command}".`, child.error.message);
// A signalled child reports status === null; exiting 0 there would hide the failure.
process.exit(child.signal ? 1 : child.status ?? 1);