feat: mirrors

This commit is contained in:
tresabhi committed 2025-04-01 21:03:39 -05:00
1 parent 8087752172
commit a8dc3313cf
16 files changed
+268 -148

No files matched your search

-6
View File
@@ -1,13 +1,7 @@
syntax = "proto2";
import "packages/core/src/protos/tank.proto";
package blitzkit;
message Tanks {
repeated string tanks = 1;
}
message TanksFull {
repeated Tank tanks = 1;
}
@@ -0,0 +1,9 @@
syntax = "proto2";
import "packages/core/src/protos/tank.proto";
package blitzkit;
message TanksFull {
repeated Tank tanks = 1;
}
@@ -1,7 +1,6 @@
import { fetchPB, type MessageFns } from '@blitzkit/core';
import type { APIRoute } from 'astro';
export async function blobMirror(url: string) {
export async function blobProxy(url: string) {
if (import.meta.env.MODE === 'development') {
return Response.redirect(url);
} else {
@@ -11,16 +10,23 @@ export async function blobMirror(url: string) {
}
}
export function apiMirror(path: string) {
const get: APIRoute = () => blobMirror(`http://localhost:5000${path}`);
return get;
export function apiBlobProxy(path: string) {
return blobProxy(`http://localhost:5000${path}`);
}
export function jsonMirror<Type>(path: string, message: MessageFns<Type>) {
const get: APIRoute = async () => {
return new Response(
JSON.stringify(await fetchPB(`http://localhost:5000${path}`, message)),
);
};
return get;
export function apiBlobProxyCurry(path: string) {
return () => apiBlobProxy(path);
}
export async function jsonMirror<Type>(
path: string,
message: MessageFns<Type>,
) {
return new Response(
JSON.stringify(await fetchPB(`http://localhost:5000${path}`, message)),
);
}
export function jsonMirrorCurry<Type>(path: string, message: MessageFns<Type>) {
return () => jsonMirror(path, message);
}
@@ -1,7 +1,21 @@
import { OpenInNewWindowIcon } from '@radix-ui/react-icons';
import { Box, Code, IconButton, Link, Table } from '@radix-ui/themes';
import { useState } from 'react';
import { Var } from '../../../core/radix/var';
import { assertSecret } from '@blitzkit/core';
import {
FileTextIcon,
OpenInNewWindowIcon,
RocketIcon,
} from '@radix-ui/react-icons';
import {
Button,
Code,
Dialog,
Flex,
IconButton,
Inset,
Link,
Separator,
Table,
} from '@radix-ui/themes';
import { BlitzKitTheme } from '../../../components/BlitzKitTheme';
import type { APIPath } from './index.astro';
interface SlugsTableProps {
@@ -9,13 +23,12 @@ interface SlugsTableProps {
path: string;
}
const MAX_UNEXPANDED = 3;
export function SlugsTable({ slugs, path }: SlugsTableProps) {
if (slugs === undefined) return null;
const [expanded, setExpanded] = useState(false);
interface TableRootProps {
slugs: NonNullable<APIPath['slugs']>;
path: string;
}
function TableRoot({ slugs, path }: TableRootProps) {
return (
<Table.Root variant="surface">
<Table.Header>
@@ -34,7 +47,6 @@ export function SlugsTable({ slugs, path }: SlugsTableProps) {
<Table.Body>
{slugs.groups.map((group, groupIndex) => {
if (!expanded && groupIndex >= MAX_UNEXPANDED) return null;
let draftPath = path;
slugs.titles.forEach((title) => {
@@ -60,53 +72,84 @@ export function SlugsTable({ slugs, path }: SlugsTableProps) {
</Table.Cell>
))}
<Table.Cell>
<Table.Cell align="right">
<Link target="_blank" href={`/api/${draftPath}`}>
<IconButton variant="surface">
<OpenInNewWindowIcon />
<IconButton variant="solid">
<RocketIcon />
</IconButton>
</Link>
</Table.Cell>
</Table.Row>
);
})}
<Table.Row style={{ position: 'relative' }}>
{!expanded && (
<Box
width="100%"
height="100%"
position="absolute"
top="0"
style={{
transform: 'translate(0, -100%)',
background: `linear-gradient(${Var('black-a1')}, ${Var('black-a5')})`,
}}
/>
)}
<Table.Cell />
<Link
href="#"
underline="always"
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
onClick={(event) => {
event.preventDefault();
setExpanded((state) => !state);
}}
>
{expanded
? 'Show less'
: `View all ${slugs.groups.length.toLocaleString('en')} slugs`}
</Link>
</Table.Row>
</Table.Body>
</Table.Root>
);
}
export function SlugsTable({ slugs, path }: SlugsTableProps) {
if (slugs === undefined) return null;
return (
<BlitzKitTheme>
<Dialog.Root>
<Dialog.Trigger>
<Button color="amber">
Slugs <OpenInNewWindowIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Inset>
<TableRoot slugs={slugs} path={path} />
</Inset>
</Dialog.Content>
</Dialog.Root>
</BlitzKitTheme>
);
}
interface SchemaProps {
schema: string;
name: string;
}
export function Schema({ schema, name }: SchemaProps) {
return (
<BlitzKitTheme>
<Dialog.Root>
<Dialog.Trigger>
<Button color="amber" variant="outline">
Schema <FileTextIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Flex direction="column" gap="5">
<Link
target="_blank"
href={`https://github.com/${assertSecret(
import.meta.env.PUBLIC_REPO,
)}/blob/${assertSecret(
import.meta.env.PUBLIC_BRANCH,
)}/packages/core/src/protos/${name}.proto`}
>
<Code>{name}.proto</Code> on GitHub <OpenInNewWindowIcon />
</Link>
<Separator size="4" />
<Code
color="gray"
highContrast
variant="ghost"
style={{ whiteSpace: 'pre' }}
>
{schema}
</Code>
</Flex>
</Dialog.Content>
</Dialog.Root>
</BlitzKitTheme>
);
}
@@ -1,6 +1,7 @@
---
import { OpenInNewWindowIcon } from '@radix-ui/react-icons';
import { RocketIcon } from '@radix-ui/react-icons';
import {
Button,
Code,
Flex,
Heading,
@@ -10,10 +11,11 @@ import {
type CodeProps,
} from '@radix-ui/themes';
import type { GetStaticPaths, GetStaticPathsOptions } from 'astro';
import { readFile } from 'fs/promises';
import { PageWrapper } from '../../../components/PageWrapper';
import { apiTool } from '../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
import { SlugsTable } from './_index';
import { Schema, SlugsTable } from './_index';
export { getStaticPaths } from '../index.astro';
@@ -32,6 +34,10 @@ export interface APIPath {
titles: string[];
groups: SlugGroup[];
};
schema?: {
name: string;
content: string;
};
}
type SlugGroup = Record<string, string | number | undefined>;
@@ -88,9 +94,11 @@ for (const rawPath in globRaw) {
);
const path = rawPath.replace(ROOT, '').replace('.ts', '');
let comment = '';
let schema: APIPath['schema'];
const tags: Tag[] = [];
commentWithTags.split('\n').forEach((line) => {
for (const line of commentWithTags.split('\n')) {
const match = line.match(tagRegex);
if (match) {
@@ -103,6 +111,16 @@ for (const rawPath in globRaw) {
const firstWordMatch = descriptionRaw.match(firstWordRegex)!;
discriminator = firstWordMatch[1];
description = firstWordMatch[2];
} else if (tag === '@proto') {
const firstWordMatch = descriptionRaw.match(firstWordRegex)!;
const fileName = firstWordMatch[1];
const buffer = await readFile(
`../../packages/core/src/protos/${fileName}.proto`,
);
const content = buffer.toString();
schema = { name: fileName, content };
discriminator = firstWordMatch[1];
} else {
description = descriptionRaw;
}
@@ -115,10 +133,10 @@ for (const rawPath in globRaw) {
} else {
comment += `${line}\n`;
}
});
}
comment = comment.trim();
apis.push({ path, content, comment, tags, slugs });
apis.push({ path, content, comment, tags, slugs, schema });
}
apis.sort((a, b) => a.path.localeCompare(b.path));
@@ -131,21 +149,35 @@ apis.sort((a, b) => a.path.localeCompare(b.path));
apis.map((api, index) => (
<>
<Flex direction="column" gap="5">
<Link
href={api.slugs ? undefined : `/api/${api.path}`}
underline="always"
target="_blank"
>
<Flex align="center" justify="between">
<Heading>
<Flex align="center" gap="2">
<Code variant="ghost">{api.path}</Code>
{!api.slugs && (
<OpenInNewWindowIcon width="1em" height="1em" />
)}
</Flex>
<Code variant="ghost">{api.path}</Code>
</Heading>
</Link>
<Flex justify="end" gap="2">
{api.schema && (
<Schema
client:load
schema={api.schema.content}
name={api.schema.name}
/>
)}
{!api.slugs && (
<Link
href={`/api/${api.path}`}
underline="always"
target="_blank"
>
<Button color="amber">
Test <RocketIcon />
</Button>
</Link>
)}
<SlugsTable client:load path={api.path} slugs={api.slugs} />
</Flex>
</Flex>
<Text>{api.comment}</Text>
@@ -180,11 +212,7 @@ apis.sort((a, b) => a.path.localeCompare(b.path));
)}
</Flex>
<SlugsTable path={api.path} client:load slugs={api.slugs} />
{index < apis.length - 1 && (
<Separator size="4" style={{ height: 0.5 }} />
)}
{index < apis.length - 1 && <Separator size="4" />}
</>
))
}
+7 -2
View File
@@ -1,5 +1,6 @@
import type { APIRoute } from 'astro';
import packageJSON from '../../../../../package.json';
import { TOOLS } from '../../constants/tools';
/**
* Returns information on the current build of BlitzKit like the version
@@ -8,6 +9,10 @@ import packageJSON from '../../../../../package.json';
* @todo Add more meta data.
* @returns JSON.
*/
export const GET: APIRoute = async () => {
return Response.json({ version: packageJSON.version });
export const GET: APIRoute = async ({ request }) => {
return Response.json({
url: new URL(request.url).origin,
version: packageJSON.version,
tools: TOOLS.map((tool) => tool.id),
});
};
@@ -1,6 +1,6 @@
import { asset } from '@blitzkit/core';
import type { APIRoute } from 'astro';
import { blobMirror } from '../../../../../core/blitzkit/blobMirror';
import { blobProxy } from '../../../../../core/blitzkit/blobMirror';
export { getStaticPaths } from '../meta.json';
@@ -11,5 +11,5 @@ export { getStaticPaths } from '../meta.json';
* @returns PNG.
*/
export const GET: APIRoute<{}, { id: string }> = async ({ params }) => {
return await blobMirror(asset(`/tanks/${params.id}/icons/big.png`));
return await blobProxy(asset(`/tanks/${params.id}/icons/big.png`));
};
@@ -1,6 +1,6 @@
import { asset } from '@blitzkit/core';
import type { APIRoute } from 'astro';
import { blobMirror } from '../../../../../core/blitzkit/blobMirror';
import { blobProxy } from '../../../../../core/blitzkit/blobMirror';
export { getStaticPaths } from '../meta.json';
@@ -11,5 +11,5 @@ export { getStaticPaths } from '../meta.json';
* @returns PNG.
*/
export const GET: APIRoute<{}, { id: string }> = async ({ params }) => {
return await blobMirror(asset(`/tanks/${params.id}/icons/small.png`));
return await blobProxy(asset(`/tanks/${params.id}/icons/small.png`));
};
@@ -1,5 +1,6 @@
import { fetchTank, fetchTanks } from '@blitzkit/core';
import { fetchTanks, Tank } from '@blitzkit/core';
import type { APIRoute, GetStaticPaths } from 'astro';
import { jsonMirror } from '../../../../core/blitzkit/blobMirror';
export const getStaticPaths = (async () => {
const tanks = await fetchTanks();
@@ -7,15 +8,10 @@ export const getStaticPaths = (async () => {
}) satisfies GetStaticPaths;
/**
* Returns the meta data on a tank which includes its modules, characteristics,
* ancestry, descendants, equipment, characteristics, etc. However, this does
* not include information on the tank model. Use tanks/list.json to get all
* valid ids.
* Identical to tanks/[id]/meta.pb but in JSON.
*
* @param id The tank id.
* @returns JSON.
*/
export const GET: APIRoute<{}, { id: string }> = async ({ params }) => {
const tank = await fetchTank(params.id);
return Response.json(tank);
};
export const GET: APIRoute<{}, { id: string }> = ({ params }) =>
jsonMirror(`/tanks/${params.id}.pb`, Tank);
@@ -0,0 +1,20 @@
import { fetchTanks } from '@blitzkit/core';
import type { APIRoute, GetStaticPaths } from 'astro';
import { apiBlobProxy } from '../../../../core/blitzkit/blobMirror';
export const getStaticPaths = (async () => {
const tanks = await fetchTanks();
return tanks.tanks.map((id) => ({ params: { id } }));
}) satisfies GetStaticPaths;
/**
* Returns the meta data on a tank which includes its modules, characteristics,
* ancestry, descendants, equipment, characteristics, etc. However, this does
* not include information on the tank model. Use tanks/list.json to get all
* valid ids.
*
* @param id The tank id.
* @proto tank
*/
export const GET: APIRoute<{}, { id: string }> = ({ params }) =>
apiBlobProxy(`/tanks/${params.id}.pb`);
@@ -1,5 +1,5 @@
import { TanksFull } from '@blitzkit/core';
import { jsonMirror } from '../../../core/blitzkit/blobMirror';
import { jsonMirrorCurry } from '../../../core/blitzkit/blobMirror';
/**
* Identical to tanks/full.pb but in JSON.
@@ -7,4 +7,4 @@ import { jsonMirror } from '../../../core/blitzkit/blobMirror';
* @warning This is a large response. Consider using tanks/[id]/meta.json for individual tanks.
* @returns JSON.
*/
export const GET = jsonMirror('/tanks/full.pb', TanksFull);
export const GET = jsonMirrorCurry('/tanks/full.pb', TanksFull);
@@ -1,8 +1,8 @@
import { apiMirror } from '../../../core/blitzkit/blobMirror';
import { apiBlobProxyCurry } from '../../../core/blitzkit/blobMirror';
/**
* Returns meta data similar to tanks/[id].json but for all tanks as an array. There is no guarantee of order.
*
* @proto TanksFull
* @proto tanks_full
*/
export const GET = apiMirror('/tanks/full.pb');
export const GET = apiBlobProxyCurry('/tanks/full.pb');
@@ -1,9 +1,9 @@
import { Tanks } from '@blitzkit/core';
import { jsonMirror } from '../../../core/blitzkit/blobMirror';
import { jsonMirrorCurry } from '../../../core/blitzkit/blobMirror';
/**
* Identical to tanks/list.pb but in JSON.
*
* @returns JSON.
*/
export const GET = jsonMirror('/tanks/list.pb', Tanks);
export const GET = jsonMirrorCurry('/tanks/list.pb', Tanks);
@@ -1,8 +1,8 @@
import { apiMirror } from '../../../core/blitzkit/blobMirror';
import { apiBlobProxyCurry } from '../../../core/blitzkit/blobMirror';
/**
* Returns a list of all tank ids. There is no guarantee of order.
*
* @returns JSON.
* @proto tanks
*/
export const GET = apiMirror('/tanks/list.pb');
export const GET = apiBlobProxyCurry('/tanks/list.pb');
+19 -10
View File
@@ -1,5 +1,9 @@
using System.Threading.Tasks;
using AssetRipper.TextureDecoder.Rgb.Channels;
using Blitzkit;
using Google.Protobuf;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace BlitzKit.CLI.Functions
{
@@ -9,19 +13,19 @@ namespace BlitzKit.CLI.Functions
readonly Mangler mangler = new(args);
void Map<T>(string path, Func<Task<T>> func)
where T : IMessage<T> =>
app.MapGet(path, async context => await Octet(context, await func()));
void Map<T>(string path, Func<T> func)
where T : IMessage<T> => Map(path, () => Task.FromResult(func()));
static async Task Octet<T>(HttpContext context, T message)
where T : IMessage<T>
{
app.MapGet(
path,
async (context) =>
{
var message = await func();
var bytes = message.ToByteArray();
var bytes = message.ToByteArray();
context.Response.ContentType = "application/octet-stream";
await context.Response.Body.WriteAsync(bytes);
}
);
context.Response.ContentType = "application/octet-stream";
await context.Response.Body.WriteAsync(bytes);
}
public async Task Run()
@@ -31,6 +35,11 @@ namespace BlitzKit.CLI.Functions
Map("/tanks/full.pb", mangler.TanksFull);
Map("/tanks/list.pb", mangler.Tanks);
app.MapGet(
"/tanks/{id}.pb",
(HttpContext context, string id) => Octet(context, mangler.Tank(id))
);
app.Run();
}
}
+32 -22
View File
@@ -49,11 +49,21 @@ namespace BlitzKit.CLI.Functions
readonly BlitzProvider provider = new(args.Contains("--depot"));
Locales? locales;
readonly Dictionary<string, Dictionary<string, string>> strings = [];
TanksFull tanksFull = null!;
public async Task<TanksFull> TanksFull()
public async Task Initialize()
{
await FetchStrings();
tanksFull = TanksFull();
}
public TanksFull TanksFull()
{
if (tanksFull != null)
return tanksFull;
var dirs = provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories;
TanksFull tanksFull = new();
tanksFull = new();
foreach (var dir in dirs)
{
@@ -62,7 +72,7 @@ namespace BlitzKit.CLI.Functions
foreach (var tankDir in dir.Value.Directories)
{
var tank = MangleTank(tankDir.Value);
var tank = Tank(tankDir.Value);
tanksFull.Tanks.Add(tank);
}
}
@@ -72,7 +82,25 @@ namespace BlitzKit.CLI.Functions
return tanksFull;
}
Tank MangleTank(VFS tankDir)
public Tanks Tanks()
{
var tanksFull = TanksFull();
Tanks tanks = new();
foreach (var tank in tanksFull.Tanks)
{
tanks.Tanks_.Add(tank.Id);
}
return tanks;
}
public Tank Tank(string id)
{
return tanksFull.Tanks.First(tank => tank.Id == id);
}
Tank Tank(VFS tankDir)
{
var pdaName = $"PDA_{tankDir.Name}";
var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}");
@@ -94,24 +122,6 @@ namespace BlitzKit.CLI.Functions
return tank;
}
public async Task<Tanks> Tanks()
{
var tanksFull = await TanksFull();
Tanks tanks = new();
foreach (var tank in tanksFull.Tanks)
{
tanks.Tanks_.Add(tank.Id);
}
return tanks;
}
public async Task Initialize()
{
await FetchStrings();
}
///////////////////////////////////////////////////////////////////////////
I18n GetString(string name)