Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+127
-34
@@ -5,7 +5,13 @@ import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import type Database from 'better-sqlite3';
|
||||
import { FIXTURE_KEY, FIXTURE_VERSION, fixtureServers, refreshFixtureData, seedFixtureData } from './fixture-data.js';
|
||||
import {
|
||||
FIXTURE_KEY,
|
||||
FIXTURE_VERSION,
|
||||
fixtureServers,
|
||||
refreshFixtureData,
|
||||
seedFixtureData
|
||||
} from './fixture-data.js';
|
||||
import { parseFixtureOptions, reserveFixture } from './fixture-options.js';
|
||||
|
||||
const help = `Create an isolated synthetic UI preview database.
|
||||
@@ -22,7 +28,10 @@ No web server, worker, game agent or network request is started by this command.
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(help); return; }
|
||||
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
||||
console.log(help);
|
||||
return;
|
||||
}
|
||||
const options = parseFixtureOptions(args);
|
||||
process.umask(0o077);
|
||||
const destination = await reserveFixture(options.dbPath);
|
||||
@@ -30,70 +39,154 @@ async function main() {
|
||||
let closeDb: (() => void) | undefined;
|
||||
let complete = false;
|
||||
try {
|
||||
const paths = { DB_PATH: destination.dbPath, SERVERS_ROOT: join(destination.root, 't5'), T6_SERVERS_ROOT: join(destination.root, 't6'),
|
||||
BETA_ARCHIVE_DIR: join(destination.root, 'beta-archive'), GAME_FILES_ROOT: join(destination.root, 'game-files'), SQLITE_TMPDIR: join(destination.root, 'sqlite-tmp') };
|
||||
const paths = {
|
||||
DB_PATH: destination.dbPath,
|
||||
SERVERS_ROOT: join(destination.root, 't5'),
|
||||
T6_SERVERS_ROOT: join(destination.root, 't6'),
|
||||
BETA_ARCHIVE_DIR: join(destination.root, 'beta-archive'),
|
||||
GAME_FILES_ROOT: join(destination.root, 'game-files'),
|
||||
SQLITE_TMPDIR: join(destination.root, 'sqlite-tmp')
|
||||
};
|
||||
for (const [key, value] of Object.entries(paths)) {
|
||||
process.env[key] = value;
|
||||
if (key !== 'DB_PATH') await mkdir(value, { mode: 0o700 });
|
||||
}
|
||||
Object.assign(process.env, { RCON_PASSWORD: '', RCON_HOST: '127.0.0.1', EC_AUTH_ALLOW_HTTP: 'true',
|
||||
DEPLOY_KEEP_DAYS: '7', DEPLOYMENT_RESTART_CONCURRENCY: '1', DEPLOYMENT_CREATE_CONCURRENCY: '1',
|
||||
EC_SQLITE_MMAP_MB: '0', PLUTAINER_IMAGE: 'fixture.invalid/electric-cherry/preview:local' });
|
||||
Object.assign(process.env, {
|
||||
RCON_PASSWORD: '',
|
||||
RCON_HOST: '127.0.0.1',
|
||||
EC_AUTH_ALLOW_HTTP: 'true',
|
||||
DEPLOY_KEEP_DAYS: '7',
|
||||
DEPLOYMENT_RESTART_CONCURRENCY: '1',
|
||||
DEPLOYMENT_CREATE_CONCURRENCY: '1',
|
||||
EC_SQLITE_MMAP_MB: '0',
|
||||
PLUTAINER_IMAGE: 'fixture.invalid/electric-cherry/preview:local'
|
||||
});
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
vite = await createServer({ root: repository, cacheDir: join(destination.root, 'vite-cache'), configFile: false, envFile: false, appType: 'custom',
|
||||
vite = await createServer({
|
||||
root: repository,
|
||||
cacheDir: join(destination.root, 'vite-cache'),
|
||||
configFile: false,
|
||||
envFile: false,
|
||||
appType: 'custom',
|
||||
// Vite 5 otherwise opens its HMR WebSocket port even with hmr:false.
|
||||
// Attach it to an HTTP server that is deliberately never listened on.
|
||||
server: { middlewareMode: true, hmr: { server: createHttpServer() }, watch: null }, optimizeDeps: { noDiscovery: true },
|
||||
resolve: { alias: { $lib: join(repository, 'src/lib'), $ui: join(repository, 'src/lib/ui') } },
|
||||
logLevel: 'error' });
|
||||
server: { middlewareMode: true, hmr: { server: createHttpServer() }, watch: null },
|
||||
optimizeDeps: { noDiscovery: true },
|
||||
resolve: {
|
||||
alias: { $lib: join(repository, 'src/lib'), $ui: join(repository, 'src/lib/ui') }
|
||||
},
|
||||
logLevel: 'error'
|
||||
});
|
||||
// Load the real migrations through Vite's ?raw support. Importing hooks
|
||||
// would start queues and agents; this script never loads the app router.
|
||||
const database = await vite.ssrLoadModule('/src/lib/server/db/index.ts') as { db: () => Database.Database; closeDb: () => void };
|
||||
const database = (await vite.ssrLoadModule('/src/lib/server/db/index.ts')) as {
|
||||
db: () => Database.Database;
|
||||
closeDb: () => void;
|
||||
};
|
||||
closeDb = database.closeDb;
|
||||
const handle = database.db();
|
||||
const hosts = await vite.ssrLoadModule('/src/lib/server/hosts/schema.ts');
|
||||
const auth = await vite.ssrLoadModule('/src/lib/server/auth/schema.ts');
|
||||
const files = await vite.ssrLoadModule('/src/lib/server/game-files/schema.ts');
|
||||
hosts.ensureHostSchema(handle); auth.ensureAuthSchema(handle); files.ensureGameFileSchema(handle);
|
||||
hosts.ensureHostSchema(handle);
|
||||
auth.ensureAuthSchema(handle);
|
||||
files.ensureGameFileSchema(handle);
|
||||
seedFixtureData(handle, options.now);
|
||||
const password = randomBytes(24).toString('base64url');
|
||||
const { hashPassword } = await vite.ssrLoadModule('/src/lib/server/auth/passwords.js');
|
||||
handle.prepare(`INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at)
|
||||
VALUES('fixture-admin','demo','Demo operator',?,'admin',?,?)`).run(await hashPassword(password), options.now, options.now);
|
||||
if ((handle.pragma('foreign_key_check') as unknown[]).length || handle.pragma('integrity_check', { simple: true }) !== 'ok') {
|
||||
handle
|
||||
.prepare(
|
||||
`INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at)
|
||||
VALUES('fixture-admin','demo','Demo operator',?,'admin',?,?)`
|
||||
)
|
||||
.run(await hashPassword(password), options.now, options.now);
|
||||
if (
|
||||
(handle.pragma('foreign_key_check') as unknown[]).length ||
|
||||
handle.pragma('integrity_check', { simple: true }) !== 'ok'
|
||||
) {
|
||||
throw new Error('Fixture integrity validation failed.');
|
||||
}
|
||||
const environment = { ...paths, RCON_PASSWORD: '', RCON_HOST: '127.0.0.1', EC_AUTH_ALLOW_HTTP: 'true',
|
||||
PLUTAINER_IMAGE: process.env.PLUTAINER_IMAGE! };
|
||||
const environment = {
|
||||
...paths,
|
||||
RCON_PASSWORD: '',
|
||||
RCON_HOST: '127.0.0.1',
|
||||
EC_AUTH_ALLOW_HTTP: 'true',
|
||||
PLUTAINER_IMAGE: process.env.PLUTAINER_IMAGE!
|
||||
};
|
||||
const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
||||
await writeFile(join(destination.root, 'preview.env'), Object.entries(environment).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join('\n') + '\n', { flag: 'wx', mode: 0o600 });
|
||||
await writeFile(join(destination.root, 'fixture.json'), JSON.stringify({ format: FIXTURE_VERSION, marker: FIXTURE_KEY,
|
||||
dbPath: destination.dbPath, createdAt: new Date(options.now).toISOString(), username: 'demo', password,
|
||||
hosts: 3, servers: fixtureServers.map(server => server.name), matches: 500,
|
||||
note: 'Synthetic local data only. Host credentials cannot authenticate; game images and library IDs are fictitious.' }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
||||
await writeFile(
|
||||
join(destination.root, 'preview.env'),
|
||||
Object.entries(environment)
|
||||
.map(([key, value]) => `export ${key}=${shellQuote(value)}`)
|
||||
.join('\n') + '\n',
|
||||
{ flag: 'wx', mode: 0o600 }
|
||||
);
|
||||
await writeFile(
|
||||
join(destination.root, 'fixture.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
format: FIXTURE_VERSION,
|
||||
marker: FIXTURE_KEY,
|
||||
dbPath: destination.dbPath,
|
||||
createdAt: new Date(options.now).toISOString(),
|
||||
username: 'demo',
|
||||
password,
|
||||
hosts: 3,
|
||||
servers: fixtureServers.map((server) => server.name),
|
||||
matches: 500,
|
||||
note: 'Synthetic local data only. Host credentials cannot authenticate; game images and library IDs are fictitious.'
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n',
|
||||
{ flag: 'wx', mode: 0o600 }
|
||||
);
|
||||
handle.pragma('wal_checkpoint(TRUNCATE)');
|
||||
complete = true;
|
||||
console.log(`Created ${destination.dbPath}\n3 hosts · 12 servers · 500 matches · 7 days of metrics\nLocal sign-in details: ${join(destination.root, 'fixture.json')}\nPreview environment: ${join(destination.root, 'preview.env')}`);
|
||||
console.log(
|
||||
`Created ${destination.dbPath}\n3 hosts · 12 servers · 500 matches · 7 days of metrics\nLocal sign-in details: ${join(destination.root, 'fixture.json')}\nPreview environment: ${join(destination.root, 'preview.env')}`
|
||||
);
|
||||
if (options.watch) {
|
||||
refreshFixtureData(handle, Date.now());
|
||||
console.log('Refreshing synthetic observations every 15 seconds. Leave this terminal open; Ctrl+C stops the feeder.');
|
||||
console.log(
|
||||
'Refreshing synthetic observations every 15 seconds. Leave this terminal open; Ctrl+C stops the feeder.'
|
||||
);
|
||||
await new Promise<void>((resolvePromise, reject) => {
|
||||
const finish = (error?: unknown) => { clearInterval(timer); process.off('SIGINT', stop); process.off('SIGTERM', stop); if (error) reject(error); else resolvePromise(); };
|
||||
const finish = (error?: unknown) => {
|
||||
clearInterval(timer);
|
||||
process.off('SIGINT', stop);
|
||||
process.off('SIGTERM', stop);
|
||||
if (error) reject(error);
|
||||
else resolvePromise();
|
||||
};
|
||||
const stop = () => finish();
|
||||
const timer = setInterval(() => { try { refreshFixtureData(handle, Date.now()); } catch (error) { finish(error); } }, 15_000);
|
||||
process.on('SIGINT', stop); process.on('SIGTERM', stop);
|
||||
const timer = setInterval(() => {
|
||||
try {
|
||||
refreshFixtureData(handle, Date.now());
|
||||
} catch (error) {
|
||||
finish(error);
|
||||
}
|
||||
}, 15_000);
|
||||
process.on('SIGINT', stop);
|
||||
process.on('SIGTERM', stop);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
try { closeDb?.(); }
|
||||
finally {
|
||||
try { await vite?.close(); }
|
||||
finally { if (!complete) await rm(destination.root, { recursive: true, force: true }); }
|
||||
try {
|
||||
closeDb?.();
|
||||
} finally {
|
||||
try {
|
||||
await vite?.close();
|
||||
} finally {
|
||||
if (!complete) await rm(destination.root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(`Fixture creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
`Fixture creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user