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,
+ })
+ ),
+ }),
+});