diff --git a/swc-plugins/.eslintrc.json b/swc-plugins/.eslintrc.json index 0e81f9b..9ecac64 100644 --- a/swc-plugins/.eslintrc.json +++ b/swc-plugins/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "next/core-web-vitals" -} \ No newline at end of file + "extends": "next/core-web-vitals", + "rules": { + "@typescript-eslint/no-unused-vars": "off" + } +} diff --git a/swc-plugins/app/compat/core/[version]/page.tsx b/swc-plugins/app/compat/core/[version]/page.tsx new file mode 100644 index 0000000..5e4fd23 --- /dev/null +++ b/swc-plugins/app/compat/core/[version]/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { apiClient } from "@/lib/trpc/web-client"; +import Link from "next/link"; + +export default function Page({ + params: { version }, +}: { + params: { version: string }; +}) { + const [compatRange] = apiClient.compatRange.byVersion.useSuspenseQuery({ + version, + }); + + return ( +
+ {compatRange ? ( + <> + + {compatRange.from} ~ {compatRange.to} + + + ) : ( + <> +

No compat range found

+ + )} +
+ ); +} + +export const dynamic = "force-dynamic"; diff --git a/swc-plugins/app/compat/range/[compatRangeId]/page.tsx b/swc-plugins/app/compat/range/[compatRangeId]/page.tsx new file mode 100644 index 0000000..9b34124 --- /dev/null +++ b/swc-plugins/app/compat/range/[compatRangeId]/page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { apiClient } from "@/lib/trpc/web-client"; + +export default function Page({ + params: { compatRangeId }, +}: { + params: { compatRangeId: string }; +}) { + const [compatRange] = apiClient.compatRange.get.useSuspenseQuery({ + id: BigInt(compatRangeId), + }); + + return ( +
+

+ swc_core@{compatRange.from} -{" "} + {compatRange.to} +

+ +

Runtimes

+ + +

Plugins

+ +
+ ); +} + +export const dynamic = "force-dynamic"; diff --git a/swc-plugins/app/compat/range/page.tsx b/swc-plugins/app/compat/range/page.tsx new file mode 100644 index 0000000..157ea5d --- /dev/null +++ b/swc-plugins/app/compat/range/page.tsx @@ -0,0 +1,26 @@ +import { createCaller } from "@/lib/server"; +import Link from "next/link"; + +export default async function Page() { + const api = await createCaller(); + const ranges = await api.compatRange.list(); + + return ( +
+

Compat Ranges

+ +
+ ); +} + +export const dynamic = "force-dynamic"; +export const fetchCache = "force-no-store"; diff --git a/swc-plugins/app/loading.tsx b/swc-plugins/app/loading.tsx new file mode 100644 index 0000000..1255263 --- /dev/null +++ b/swc-plugins/app/loading.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+
Loading...
+
+ ); +} diff --git a/swc-plugins/lib/api/compatRange/router.ts b/swc-plugins/lib/api/compatRange/router.ts new file mode 100644 index 0000000..8972132 --- /dev/null +++ b/swc-plugins/lib/api/compatRange/router.ts @@ -0,0 +1,167 @@ +import { publicProcedure, router } from "@/lib/base"; +import { db } from "@/lib/prisma"; +import semver from "semver"; +import { z } from "zod"; +import { VersionRange, VersionRangeSchema } from "./zod"; + +export const compatRangeRouter = router({ + list: publicProcedure + .input(z.void()) + .output( + z.array( + z.object({ + id: z.bigint(), + from: z.string(), + to: z.string(), + }) + ) + ) + .query(async ({ ctx }) => { + const versions = await db.compatRange.findMany({ + orderBy: { + from: "asc", + }, + }); + + return versions; + }), + + get: publicProcedure + .input( + z.object({ + id: z.bigint(), + }) + ) + .output( + z.object({ + id: z.bigint(), + from: z.string(), + to: z.string(), + plugins: z.array(VersionRangeSchema), + runtimes: z.array(VersionRangeSchema), + }) + ) + .query(async ({ ctx, input: { id } }) => { + const range = await db.compatRange.findUnique({ + where: { + id: id, + }, + select: { + id: true, + from: true, + to: true, + plugins: { + select: { + id: true, + version: true, + plugin: { + select: { + name: true, + }, + }, + }, + }, + runtimes: { + select: { + id: true, + version: true, + runtime: { + select: { + name: true, + }, + }, + }, + }, + }, + }); + + if (!range) { + throw new Error("CompatRange not found"); + } + const plugins = merge( + range.plugins.map((p) => ({ name: p.plugin.name, version: p.version })) + ); + const runtimes = merge( + range.runtimes.map((p) => ({ + name: p.runtime.name, + version: p.version, + })) + ); + + return { + id: range.id, + from: range.from, + to: range.to, + plugins, + runtimes, + }; + }), + + byVersion: publicProcedure + .input( + z.object({ + version: z.string(), + }) + ) + .output( + z.nullable( + z.object({ + id: z.bigint(), + from: z.string(), + to: z.string(), + }) + ) + ) + .query(async ({ ctx, input: { version } }) => { + const versions = await db.compatRange.findMany({ + select: { + id: true, + from: true, + to: true, + }, + }); + + for (const range of versions) { + if (semver.lt(version, range.from)) { + continue; + } + + if (semver.gte(version, range.to)) { + return range; + } + } + + return null; + }), +}); + +function merge(ranges: { name: string; version: string }[]): VersionRange[] { + const merged: { [key: string]: VersionRange } = {}; + + for (const { name, version } of ranges) { + if (!merged[name]) { + merged[name] = { name, minVersion: "0.0.0", maxVersion: "0.0.0" }; + } + + const { min, max } = mergeVersion( + merged[name].minVersion, + merged[name].maxVersion, + version + ); + merged[name] = { name, minVersion: min, maxVersion: max }; + } + + return Object.values(merged); +} +/** + * + * @param min semver + * @param max semver + * @param newValue semver + */ +function mergeVersion(min: string, max: string, newValue: string) { + const minVersion = semver.lt(min, newValue) ? min : newValue; + const maxVersion = semver.gt(max, newValue) ? max : newValue; + + return { min: minVersion, max: maxVersion }; +} diff --git a/swc-plugins/lib/api/compatRange/zod.ts b/swc-plugins/lib/api/compatRange/zod.ts new file mode 100644 index 0000000..0e377d1 --- /dev/null +++ b/swc-plugins/lib/api/compatRange/zod.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const VersionRangeSchema = z.object({ + name: z.string(), + minVersion: z.string(), + maxVersion: z.string(), +}); +export type VersionRange = z.infer; diff --git a/swc-plugins/lib/api/router.ts b/swc-plugins/lib/api/router.ts index 87c3513..855a170 100644 --- a/swc-plugins/lib/api/router.ts +++ b/swc-plugins/lib/api/router.ts @@ -1,10 +1,13 @@ import { router } from "@/lib/base"; import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; +import { compatRangeRouter } from "./compatRange/router"; import { userRouter } from "./users/router"; export const apiRouter = router({ users: userRouter, + + compatRange: compatRangeRouter, }); export type ApiRouter = typeof apiRouter; diff --git a/swc-plugins/lib/utils.ts b/swc-plugins/lib/utils.ts index d084cca..04cccc9 100644 --- a/swc-plugins/lib/utils.ts +++ b/swc-plugins/lib/utils.ts @@ -1,6 +1,11 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); +} + +export function typed(schema: z.ZodSchema, data: T): T { + return schema.parse(data); } diff --git a/swc-plugins/package.json b/swc-plugins/package.json index 05927d4..319fa46 100644 --- a/swc-plugins/package.json +++ b/swc-plugins/package.json @@ -68,6 +68,7 @@ "react-hook-form": "^7.52.0", "react-resizable-panels": "^2.1.1", "recharts": "^2.12.7", + "semver": "^7.6.3", "sharp": "^0.33.2", "sonner": "^1.5.0", "superjson": "^2.2.1", @@ -85,6 +86,7 @@ "@types/node": "20.14.2", "@types/react": "18.3.3", "@types/react-dom": "18.2.18", + "@types/semver": "^7.5.8", "autoprefixer": "10.4.17", "postcss": "8.4.33", "prisma": "^5.17.0", diff --git a/swc-plugins/prisma/schema.prisma b/swc-plugins/prisma/schema.prisma index 9d7caf5..68de2e1 100644 --- a/swc-plugins/prisma/schema.prisma +++ b/swc-plugins/prisma/schema.prisma @@ -1,6 +1,6 @@ datasource db { provider = "postgresql" - url = env("DATABASE_URL") + url = env("POSTGRES_PRISMA_URL") } generator client { @@ -131,3 +131,53 @@ model TeamInvitation { @@unique(name: "emailPerTeam", [teamId, email]) } + +/// The range of versions of the `swc_core` that a plugin is compatible with. +model CompatRange { + id BigInt @id @default(autoincrement()) + from String + to String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + runtimes SwcRuntimeVersion[] + plugins SwcPluginVersion[] +} + +/// Plugin runner +model SwcRuntime { + id BigInt @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + versions SwcRuntimeVersion[] +} + +model SwcRuntimeVersion { + id BigInt @id @default(autoincrement()) + runtime SwcRuntime @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + runtimeId BigInt + version String + compatRange CompatRange @relation(fields: [compatRangeId], references: [id]) + compatRangeId BigInt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model SwcPlugin { + id BigInt @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + versions SwcPluginVersion[] +} + +model SwcPluginVersion { + id BigInt @id @default(autoincrement()) + plugin SwcPlugin @relation(fields: [pluginId], references: [id], onDelete: Cascade) + pluginId BigInt + version String + compatRange CompatRange @relation(fields: [compatRangeId], references: [id]) + compatRangeId BigInt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +}