Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":noReviewer",
":noAssignees"
],
"extends": ["config:base", ":noReviewer", ":noAssignees"],
"packageRules": [
{
"packagePatterns": ["*"],
Expand Down
30 changes: 2 additions & 28 deletions src/components/package-port/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
createSystem,
createVirtualTypeScriptEnvironment,
} from "@typescript/vfs"
import { loadDefaultLibMap } from "@/lib/ts-lib-cache"
import { loadDefaultLibMap, fetchWithPackageCaching } from "@/lib/ts-lib-cache"
import { tsAutocomplete, tsFacet, tsSync } from "@valtown/codemirror-ts"
import { getLints } from "@valtown/codemirror-ts"
import { EditorView } from "codemirror"
Expand Down Expand Up @@ -219,33 +219,7 @@ export const CodeEditor = ({
projectName: "my-project",
typescript: tsModule,
logger: console,
fetcher: (async (input: RequestInfo | URL, init?: RequestInit) => {
const registryPrefixes = [
"https://data.jsdelivr.com/v1/package/resolve/npm/@tsci/",
"https://data.jsdelivr.com/v1/package/npm/@tsci/",
"https://cdn.jsdelivr.net/npm/@tsci/",
]
if (
typeof input === "string" &&
registryPrefixes.some((prefix) => input.startsWith(prefix))
) {
const fullPackageName = input
.replace(registryPrefixes[0], "")
.replace(registryPrefixes[1], "")
.replace(registryPrefixes[2], "")
const packageName = fullPackageName.split("/")[0].replace(/\./, "/")
const pathInPackage = fullPackageName.split("/").slice(1).join("/")
const jsdelivrPath = `${packageName}${
pathInPackage ? `/${pathInPackage}` : ""
}`
return fetch(
`${apiUrl}/snippets/download?jsdelivr_resolve=${input.includes(
"/resolve/",
)}&jsdelivr_path=${encodeURIComponent(jsdelivrPath)}`,
)
}
return fetch(input, init)
}) as typeof fetch,
fetcher: fetchWithPackageCaching as typeof fetch,
delegate: {
started: () => {
const manualEditsTypeDeclaration = `
Expand Down
129 changes: 122 additions & 7 deletions src/lib/ts-lib-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ts from "typescript"

const TS_LIB_VERSION = "5.6.3"
const CACHE_PREFIX = `ts-lib-${TS_LIB_VERSION}-`
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000 // 7 days

export async function loadDefaultLibMap(): Promise<Map<string, string>> {
const fsMap = new Map<string, string>()
Expand All @@ -18,9 +19,23 @@ export async function loadDefaultLibMap(): Promise<Map<string, string>> {
const missing: string[] = []

for (const lib of libs) {
const cached = await get(CACHE_PREFIX + lib)
if (cached) {
fsMap.set("/" + lib, decompressFromUTF16(cached as string))
const cacheKey = CACHE_PREFIX + lib
const cached = await get(cacheKey)
if (
cached &&
typeof cached === "object" &&
"compressedFileContent" in cached &&
"timestamp" in cached
) {
const { compressedFileContent, timestamp } = cached as {
compressedFileContent: string
timestamp: number
}
if (Date.now() - timestamp < CACHE_TTL) {
fsMap.set("/" + lib, decompressFromUTF16(compressedFileContent))
} else {
missing.push(lib)
}
} else {
missing.push(lib)
}
Expand All @@ -36,12 +51,112 @@ export async function loadDefaultLibMap(): Promise<Map<string, string>> {
)
for (const [filename, content] of fetched) {
fsMap.set(filename, content)
await set(
CACHE_PREFIX + filename.replace(/^\//, ""),
compressToUTF16(content),
)
const cacheKey = CACHE_PREFIX + filename.replace(/^\//, "")
const compressed = compressToUTF16(content)
await set(cacheKey, {
compressedFileContent: compressed,
timestamp: Date.now(),
}).catch(() => {})
}
}

return fsMap
}

export async function fetchWithPackageCaching(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
const url = typeof input === "string" ? input : input.toString()

// Only cache GET requests for packages
if (init?.method && init.method !== "GET") {
return fetch(input, init)
}

// Check if this should be cached
const shouldCache =
url.includes("jsdelivr.net") ||
url.includes("unpkg.com") ||
url.includes("@types/") ||
url.includes("@tsci/")

if (!shouldCache) {
return fetch(input, init)
}

const cacheKey = `package-cache-${url}`

// Check cache
const cached = await get(cacheKey).catch(() => null)
if (
cached &&
typeof cached === "object" &&
"compressedFileContent" in cached &&
"timestamp" in cached
) {
const { compressedFileContent, timestamp } = cached as {
compressedFileContent: string
timestamp: number
}
if (Date.now() - timestamp < CACHE_TTL) {
return new Response(decompressFromUTF16(compressedFileContent), {
status: 200,
statusText: "OK",
})
}
}

// Handle @tsci packages
let fetchUrl = url
if (
url.includes("@tsci/") &&
(url.includes("jsdelivr.net") || url.includes("data.jsdelivr.com"))
) {
let packagePath = ""
if (url.includes("jsdelivr.net")) {
packagePath = url.replace("https://cdn.jsdelivr.net/npm/@tsci/", "")
} else if (url.includes("/v1/package/resolve/npm/@tsci/")) {
const resolveIndex = url.indexOf("/v1/package/resolve/npm/@tsci/")
packagePath = url.substring(
resolveIndex + "/v1/package/resolve/npm/@tsci/".length,
)
} else if (url.includes("/v1/package/npm/@tsci/")) {
const npmIndex = url.indexOf("/v1/package/npm/@tsci/")
packagePath = url.substring(npmIndex + "/v1/package/npm/@tsci/".length)
}

if (packagePath) {
// Convert dots to slashes in the package name part (like original logic)
const parts = packagePath.split("/")
if (parts.length > 0) {
parts[0] = parts[0].replace(/\./, "/")
}
const transformedPackagePath = parts.join("/")

const apiUrl = import.meta.env.VITE_SNIPPETS_API_URL ?? "/api"
const isResolve = url.includes("/resolve/")
fetchUrl = `${apiUrl}/snippets/download?jsdelivr_resolve=${isResolve}&jsdelivr_path=${encodeURIComponent(
transformedPackagePath,
)}`
}
}

// Fetch and cache
const response = await fetch(fetchUrl, init)
if (response.ok) {
const text = await response.text()
const compressed = compressToUTF16(text)
await set(cacheKey, {
compressedFileContent: compressed,
timestamp: Date.now(),
}).catch(() => {})
return new Response(text, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
})
}

return response
}