diff --git a/apps/swc-plugins/.gitignore b/apps/swc-plugins/.gitignore index fd3dbb5..7b18c1c 100644 --- a/apps/swc-plugins/.gitignore +++ b/apps/swc-plugins/.gitignore @@ -34,3 +34,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# scripts +.cache/ \ No newline at end of file diff --git a/apps/swc-plugins/app/api/versions/from-plugin-runner/[version]/page.tsx b/apps/swc-plugins/app/api/versions/from-plugin-runner/[version]/page.tsx deleted file mode 100644 index 5a82260..0000000 --- a/apps/swc-plugins/app/api/versions/from-plugin-runner/[version]/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export default async function Page({ - params: { version }, -}: { - params: { version: string }; -}) { - // TODO: Reverse mapping from plugin runner version to swc version - return ( -
-

swc_plugin_runner: {version}

-
- ); -} diff --git a/apps/swc-plugins/app/compat/core/[version]/page.tsx b/apps/swc-plugins/app/compat/core/[version]/page.tsx index 5e4fd23..1c52b62 100644 --- a/apps/swc-plugins/app/compat/core/[version]/page.tsx +++ b/apps/swc-plugins/app/compat/core/[version]/page.tsx @@ -8,7 +8,7 @@ export default function Page({ }: { params: { version: string }; }) { - const [compatRange] = apiClient.compatRange.byVersion.useSuspenseQuery({ + const [compatRange] = apiClient.compatRange.byCoreVersion.useSuspenseQuery({ version, }); diff --git a/apps/swc-plugins/app/import/runtime/route.ts b/apps/swc-plugins/app/import/runtime/route.ts index d5e30d8..25e13ab 100644 --- a/apps/swc-plugins/app/import/runtime/route.ts +++ b/apps/swc-plugins/app/import/runtime/route.ts @@ -35,7 +35,7 @@ export async function POST(req: NextRequest) { const api = await createCaller(); for (const version of versions) { - const compatRange = await api.compatRange.byVersion({ + const compatRange = await api.compatRange.byCoreVersion({ version: version.swcCoreVersion, }); if (!compatRange) { diff --git a/apps/swc-plugins/app/import/swc_core/route.ts b/apps/swc-plugins/app/import/swc_core/route.ts new file mode 100644 index 0000000..6715e2d --- /dev/null +++ b/apps/swc-plugins/app/import/swc_core/route.ts @@ -0,0 +1,29 @@ +import { createCaller } from "@/lib/server"; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +const CoreVersionSchema = z.object({ + version: z.string(), + pluginRunnerReq: z.string(), +}); + +const BodySchema = z.object({ + coreVersions: z.array(CoreVersionSchema), + pluginRunnerVersions: z.array(z.string()), +}); + +export async function POST(req: NextRequest) { + const api = await createCaller(); + const { coreVersions, pluginRunnerVersions } = BodySchema.parse( + await req.json() + ); + + await api.compatRange.addCacheForCrates({ + coreVersions, + pluginRunnerVersions, + }); + + return NextResponse.json({ + ok: true, + }); +} diff --git a/apps/swc-plugins/app/versions/from-plugin-runner/[version]/page.tsx b/apps/swc-plugins/app/versions/from-plugin-runner/[version]/page.tsx new file mode 100644 index 0000000..d14a3be --- /dev/null +++ b/apps/swc-plugins/app/versions/from-plugin-runner/[version]/page.tsx @@ -0,0 +1,19 @@ +import { createCaller } from "@/lib/server"; +import { redirect } from "next/navigation"; + +export default async function Page({ + params: { version }, +}: { + params: { version: string }; +}) { + const api = await createCaller(); + const compatRange = await api.compatRange.byPluginRunnerVersion({ + version, + }); + + if (compatRange) { + return redirect(`/compat/range/${compatRange.id}`); + } + + return
No compat range found for swc_plugin_runner@{version}
; +} diff --git a/apps/swc-plugins/lib/api/compatRange/router.ts b/apps/swc-plugins/lib/api/compatRange/router.ts index 55292b4..f22d936 100644 --- a/apps/swc-plugins/lib/api/compatRange/router.ts +++ b/apps/swc-plugins/lib/api/compatRange/router.ts @@ -1,21 +1,20 @@ import { publicProcedure, router } from "@/lib/base"; import { db } from "@/lib/prisma"; +import { TRPCError } from "@trpc/server"; import semver from "semver"; import { z } from "zod"; import { VersionRange, VersionRangeSchema } from "./zod"; +export const CompatRangeSchema = z.object({ + id: z.bigint(), + from: z.string(), + to: z.string(), +}); + export const compatRangeRouter = router({ list: publicProcedure .input(z.void()) - .output( - z.array( - z.object({ - id: z.bigint(), - from: z.string(), - to: z.string(), - }) - ) - ) + .output(z.array(CompatRangeSchema)) .query(async ({ ctx }) => { const versions = await db.compatRange.findMany({ orderBy: { @@ -97,23 +96,64 @@ export const compatRangeRouter = router({ }; }), - byVersion: publicProcedure + byPluginRunnerVersion: publicProcedure .input( z.object({ - version: z.string(), + version: z.string().describe("The version of the swc_plugin_runner"), }) ) - .output( - z.nullable( - z.object({ - id: z.bigint(), - from: z.string(), - to: z.string(), - }) - ) + .output(z.nullable(CompatRangeSchema)) + .query(async ({ ctx, input: { version } }) => { + const v = await db.swcPluginRunnerVersion.findUnique({ + where: { + version, + }, + select: { + compatRange: { + select: { + id: true, + from: true, + to: true, + }, + }, + }, + }); + + return v?.compatRange ?? null; + }), + + byCoreVersion: publicProcedure + .input( + z.object({ + version: z.string(), + }) ) + .output(z.nullable(CompatRangeSchema)) .query(async ({ ctx, input: { version } }) => { - const versions = await db.compatRange.findMany({ + // Try the cache first. + { + const v = await db.swcCoreVersion.findUnique({ + where: { + version, + }, + select: { + compatRange: { + select: { + id: true, + from: true, + to: true, + }, + }, + }, + }); + + if (v) { + return v.compatRange; + } + } + + console.warn("Fallback to full search"); + const compatRanges = await db.compatRange.findMany({ select: { id: true, from: true, @@ -121,7 +161,7 @@ export const compatRangeRouter = router({ }, }); - for (const range of versions) { + for (const range of compatRanges) { if ( semver.gte(version, range.from) && (range.to === "*" || semver.lte(version, range.to)) @@ -132,6 +172,115 @@ export const compatRangeRouter = router({ return null; }), + + addCacheForCrates: publicProcedure + .input( + z.object({ + pluginRunnerVersions: z.array(z.string()), + coreVersions: z.array( + z.object({ + version: z.string().describe("The version of the swc_core"), + pluginRunnerReq: z.string(), + }) + ), + }) + ) + .output(z.void()) + .mutation( + async ({ ctx, input: { coreVersions, pluginRunnerVersions } }) => { + if (process.env.NODE_ENV === "production") { + throw new TRPCError({ + code: "FORBIDDEN", + }); + } + + const previousMaxCoreVersion = await maxSwcCoreVersion(); + const previousMaxPluginRunnerVersion = + await maxSwcPluginRunnerVersion(); + + const compatRanges = await db.compatRange.findMany({ + select: { + id: true, + from: true, + to: true, + }, + }); + + const done = new Set(); + + function byVersion(swcCoreVersion: string) { + for (const range of compatRanges) { + if ( + semver.gte(swcCoreVersion, range.from) && + (range.to === "*" || semver.lte(swcCoreVersion, range.to)) + ) { + return range; + } + } + } + + for (const corePkg of coreVersions) { + corePkg.version = corePkg.version.replace("v", ""); + + if (semver.lt(corePkg.version, previousMaxCoreVersion)) { + console.log( + `Skipping swc_core@${corePkg.version} as it's less than previous max (${previousMaxCoreVersion})` + ); + continue; + } + + const compatRange = byVersion(corePkg.version); + + if (!compatRange) { + console.error(`Compat range not found for ${corePkg.version}`); + continue; + } + + for (let rv of pluginRunnerVersions) { + rv = rv.replace("v", ""); + + if (done.has(rv)) { + continue; + } + if (semver.lt(rv, previousMaxPluginRunnerVersion)) { + continue; + } + + if (semver.satisfies(rv, corePkg.pluginRunnerReq)) { + await db.swcPluginRunnerVersion.upsert({ + where: { + version: rv, + }, + create: { + version: rv, + compatRangeId: compatRange.id, + }, + update: { + compatRangeId: compatRange.id, + }, + }); + console.log(`Imported swc_plugin_runner@${rv}`); + done.add(rv); + } + } + + await db.swcCoreVersion.upsert({ + where: { + version: corePkg.version, + }, + create: { + version: corePkg.version, + pluginRunnerReq: corePkg.pluginRunnerReq, + compatRangeId: compatRange.id, + }, + update: { + pluginRunnerReq: corePkg.pluginRunnerReq, + }, + }); + console.log(`Imported swc_core@${corePkg.version}`); + } + } + ), }); function merge(ranges: { name: string; version: string }[]): VersionRange[] { @@ -165,3 +314,27 @@ function mergeVersion(min: string, max: string, newValue: string) { return { min: minVersion, max: maxVersion }; } + +async function maxSwcCoreVersion() { + const coreVersions = await db.swcCoreVersion.findMany({ + select: { + version: true, + }, + }); + + return coreVersions.reduce((max, core) => { + return semver.gt(max, core.version) ? max : core.version; + }, "0.0.0"); +} + +async function maxSwcPluginRunnerVersion() { + const pluginRunnerVersions = await db.swcPluginRunnerVersion.findMany({ + select: { + version: true, + }, + }); + + return pluginRunnerVersions.reduce((max, pluginRunner) => { + return semver.gt(max, pluginRunner.version) ? max : pluginRunner.version; + }, "0.0.0"); +} diff --git a/apps/swc-plugins/prisma/schema.prisma b/apps/swc-plugins/prisma/schema.prisma index 97efebf..886108d 100644 --- a/apps/swc-plugins/prisma/schema.prisma +++ b/apps/swc-plugins/prisma/schema.prisma @@ -134,13 +134,15 @@ model TeamInvitation { /// 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[] + id BigInt @id @default(autoincrement()) + from String + to String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + runtimes SwcRuntimeVersion[] + plugins SwcPluginVersion[] + coreVersions SwcCoreVersion[] + SwcPluginRunnerVersion SwcPluginRunnerVersion[] @@unique([from, to]) } @@ -194,3 +196,25 @@ model SwcPluginVersion { @@unique([pluginId, version]) } + +model SwcCoreVersion { + id BigInt @id @default(autoincrement()) + /// The version of the (exact) `swc_core` package. + version String @unique + /// Semver range of the `swc_plugin_runner` package that is compatible with this version of `swc_core`. + pluginRunnerReq String? + compatRange CompatRange @relation(fields: [compatRangeId], references: [id]) + compatRangeId BigInt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model SwcPluginRunnerVersion { + id BigInt @id @default(autoincrement()) + /// The version of the `swc_plugin_runner` package. + version String @unique + compatRange CompatRange @relation(fields: [compatRangeId], references: [id]) + compatRangeId BigInt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/apps/swc-plugins/scripts/import-swc-core.mjs b/apps/swc-plugins/scripts/import-swc-core.mjs new file mode 100755 index 0000000..1961894 --- /dev/null +++ b/apps/swc-plugins/scripts/import-swc-core.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +import fs from "fs/promises"; + +await fs.mkdir("./.cache", { recursive: true }); + +// Fetch .cahce/swc_core.json from https://index.crates.io/sw/c_/swc_core if it doesn't exist +async function load(crate) { + const cachePath = `./.cache/${crate}.json`; + try { + return await fs.readFile(cachePath, "utf8"); + } catch (e) { + console.log(`Cache miss for ${crate}`); + } + + const response = await fetch(`https://index.crates.io/sw/c_/${crate}`); + const content = await response.text(); + await fs.writeFile(cachePath, content, "utf8"); + return content; +} + +const [coreData, pluginRunnerData] = await Promise.all([ + load("swc_core"), + load("swc_plugin_runner"), +]); + +const requiredVersions = new Map(); + +for (const line of coreData.split("\n")) { + const data = JSON.parse(line.trim()); + + const pluginRunner = data.deps.find((d) => d.name === "swc_plugin_runner"); + if (pluginRunner) { + requiredVersions.set(data.vers, pluginRunner.req); + } +} + +const pluginRunnerVersions = pluginRunnerData + .split("\n") + .map((line) => line.trim()) + .map(JSON.parse) + .map((v) => v.vers); + +await fetch("http://localhost:50000/import/swc_core", { + method: "POST", + body: JSON.stringify({ + pluginRunnerVersions, + coreVersions: Array.from(requiredVersions.entries()).map( + ([version, req]) => ({ + version, + pluginRunnerReq: req, + }) + ), + }), +});