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
+
+ {compatRange.runtimes.map((runtime) => (
+ -
+ {runtime.name}
+ {runtime.minVersion} - {runtime.maxVersion}
+
+ ))}
+
+
+
Plugins
+
+ {compatRange.plugins.map((plugin) => (
+ -
+ {plugin.name}
+
+ ))}
+
+
+ );
+}
+
+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
+
+ {ranges.map((range) => (
+ -
+
+ swc_core@{range.from} -{" "}
+ {range.to}
+
+
+ ))}
+
+
+ );
+}
+
+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 (
+
+ );
+}
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
+}