diff --git a/codegen/server/capi_repo.ts b/codegen/server/capi_repo.ts index c056f13f6..d6565dc9c 100644 --- a/codegen/server/capi_repo.ts +++ b/codegen/server/capi_repo.ts @@ -1,5 +1,5 @@ import { PermanentMemo, TimedMemo } from "../../util/memo.ts" -import { SHA_ABBREV_LENGTH } from "./git_utils.ts" +import { getFullSha, getSha, SHA_ABBREV_LENGTH } from "./git_utils.ts" import { CodegenServer } from "./server.ts" const TAGS_TTL = 60_000 // 1 minute @@ -13,6 +13,7 @@ export const R_SHA_VERSION = /^sha:([0-9a-f]+)$/ export abstract class CapiCodegenServer extends CodegenServer { async normalizeVersion(version: string) { + if (version === this.mainVersion) return version const tagMatch = R_TAG_VERSION.exec(version) if (tagMatch) return "v" + tagMatch[1] if (R_REF_VERSION.test(version)) { @@ -28,11 +29,16 @@ export abstract class CapiCodegenServer extends CodegenServer { throw this.e404() } - moduleFileUrl(version: string, path: string) { - if (this.local && version === this.version) { + async canHandleVersion(version: string): Promise { + return version === this.mainVersion + || (await this.versionSha(version)) === (await this.versionSha(this.mainVersion)) + } + + async moduleFileUrl(version: string, path: string) { + if (this.local && (await this.canHandleVersion(version))) { return new URL("../.." + path, import.meta.url).toString() } - if (R_REF_VERSION.test(version)) { + if (R_TAG_VERSION.test(version)) { return `https://deno.land/x/capi@${version}${path}` } const shaMatch = R_SHA_VERSION.exec(version) @@ -46,7 +52,7 @@ export abstract class CapiCodegenServer extends CodegenServer { async versionSuggestions(): Promise { return [ ...new Set((await Promise.all([ - this.local ? [this.version] : [], + this.local ? [this.mainVersion] : [], this.tags(), this.branches().then(Object.keys), ])).flat()), @@ -76,6 +82,45 @@ export abstract class CapiCodegenServer extends CodegenServer { }) } + async versionSha(version: string) { + if (version === "local") { + return getSha() + } + if (R_TAG_VERSION.test(version)) { + return (await this.tagSha(version)).slice(0, SHA_ABBREV_LENGTH) + } + const shaMatch = R_SHA_VERSION.exec(version) + if (shaMatch) { + return shaMatch[1]! + } + throw new Error("expected normalized version") + } + + async versionFullSha(version: string) { + if (version === "local") { + return getFullSha() + } + if (R_TAG_VERSION.test(version)) { + return this.tagSha(version) + } + const shaMatch = R_SHA_VERSION.exec(version) + if (shaMatch) { + return await this.fullSha(shaMatch[1]!) + } + throw new Error("expected normalized version") + } + + tagShaMemo = new PermanentMemo() + tagSha(tag: string) { + return this.fullShaMemo.run(tag, async () => { + const refs: GithubRef[] = await json( + `${GITHUB_API_REPO}/git/matching-refs/tags/${tag}`, + ) + if (!refs[0]) throw this.e404() + return refs[0].object.sha + }) + } + fullShaMemo = new PermanentMemo() fullSha(sha: string) { return this.fullShaMemo.run(sha, async () => { diff --git a/codegen/server/deploy.ts b/codegen/server/deploy.ts index 7a61edd5a..e4e5ccf00 100644 --- a/codegen/server/deploy.ts +++ b/codegen/server/deploy.ts @@ -1,12 +1,5 @@ import { PermanentMemo } from "../../util/memo.ts" -import { - CapiCodegenServer, - GITHUB_API_REPO, - GithubRef, - json, - R_REF_VERSION, - R_SHA_VERSION, -} from "./capi_repo.ts" +import { CapiCodegenServer, GITHUB_API_REPO, json } from "./capi_repo.ts" import { S3Cache } from "./s3.ts" const DENO_DEPLOY_USER_ID = 75045203 @@ -23,7 +16,7 @@ export class DenoDeployCodegenServer extends CapiCodegenServer { }, this.abortController.signal) local = false - constructor(readonly version: string, moduleIndex: string[]) { + constructor(readonly mainVersion: string, moduleIndex: string[]) { super() this.moduleIndex = async () => moduleIndex } @@ -32,12 +25,12 @@ export class DenoDeployCodegenServer extends CapiCodegenServer { if (new URL(request.url).host === PRODUCTION_HOST) { return (await this.tags())[0]! } - return this.version + return this.mainVersion } deploymentUrlMemo = new PermanentMemo() async deploymentUrl(version: string) { - const fullSha = await this.versionSha(version) + const fullSha = await this.versionFullSha(version) return this.deploymentUrlMemo.run(fullSha, async () => { const deployments: GithubDeployment[] = await json( `${GITHUB_API_REPO}/deployments?sha=${fullSha}`, @@ -50,28 +43,6 @@ export class DenoDeployCodegenServer extends CapiCodegenServer { return url }) } - - async versionSha(version: string) { - if (R_REF_VERSION.test(version)) { - return this.tagSha(version) - } - const shaMatch = R_SHA_VERSION.exec(version) - if (shaMatch) { - return await this.fullSha(shaMatch[1]!) - } - throw new Error("expected normalized version") - } - - tagShaMemo = new PermanentMemo() - tagSha(tag: string) { - return this.fullShaMemo.run(tag, async () => { - const refs: GithubRef[] = await json( - `${GITHUB_API_REPO}/git/matching-refs/tags/${tag}`, - ) - if (!refs[0]) throw this.e404() - return refs[0].object.sha - }) - } } interface GithubDeployment { diff --git a/codegen/server/git_utils.ts b/codegen/server/git_utils.ts index f81beec1f..e213396a8 100644 --- a/codegen/server/git_utils.ts +++ b/codegen/server/git_utils.ts @@ -10,12 +10,16 @@ export async function getModuleIndex() { return output.split("\n").filter((x) => x.endsWith(".ts")) } -export async function getSha() { +export async function getFullSha() { const cmd = Deno.run({ cmd: ["git", "rev-parse", "@"], stdout: "piped", }) if (!(await cmd.status()).success) throw new Error("git rev-parse failed") const output = new TextDecoder().decode(await cmd.output()) - return output.slice(0, SHA_ABBREV_LENGTH) + return output +} + +export async function getSha() { + return (await getFullSha()).slice(0, SHA_ABBREV_LENGTH) } diff --git a/codegen/server/local.ts b/codegen/server/local.ts index 2081c49fc..76a90d14c 100644 --- a/codegen/server/local.ts +++ b/codegen/server/local.ts @@ -7,13 +7,13 @@ const R_DENO_LAND_URL = /^https:\/\/deno\.land\/x\/capi@(v[^\/]+)\// const R_GITHUB_URL = /^https:\/\/raw\.githubusercontent\.com\/paritytech\/capi\/([0-9a-f]+)\// export class LocalCapiCodegenServer extends CapiCodegenServer { - version + mainVersion cache: Cache = new FsCache("target/codegen", this.abortController.signal) local = true constructor(version?: string) { super() - this.version = version ?? this.detectVersion() + this.mainVersion = version ?? this.detectVersion() } detectVersion() { @@ -33,13 +33,13 @@ export class LocalCapiCodegenServer extends CapiCodegenServer { } async defaultVersion() { - return this.version + return this.mainVersion } deploymentUrlMemo = new PermanentMemo() async deploymentUrl(version: string) { return this.deploymentUrlMemo.run(version, async () => { - const mod = await import(this.moduleFileUrl(version, "/codegen/server/local.ts")) + const mod = await import(await this.moduleFileUrl(version, "/codegen/server/local.ts")) const Server = mod.LocalCapiCodegenServer as typeof LocalCapiCodegenServer const server = new Server(version) this.abortController.signal.addEventListener("abort", () => { diff --git a/codegen/server/server.ts b/codegen/server/server.ts index df7296280..dc9d89989 100644 --- a/codegen/server/server.ts +++ b/codegen/server/server.ts @@ -41,15 +41,16 @@ const R_WITH_CHAIN_URL = /^\/proxy\/(dev:\w+|wss?:[^\/]+)\/(?:@([^\/]+)\/)?(.*)$ const $index = $.array($.str) export abstract class CodegenServer { - abstract version: string abstract cache: Cache abstract local: boolean + abstract mainVersion: string + abstract canHandleVersion(version: string): Promise abstract moduleIndex(): Promise abstract defaultVersion(request: Request): Promise abstract normalizeVersion(version: string): Promise abstract deploymentUrl(version: string): Promise abstract versionSuggestions(): Promise - abstract moduleFileUrl(version: string, path: string): string + abstract moduleFileUrl(version: string, path: string): Promise abortController = new AbortController() @@ -70,7 +71,7 @@ export abstract class CodegenServer { async root(request: Request): Promise { const fullPath = new URL(request.url).pathname if (fullPath === "/.well-known/deno-import-intellisense.json") { - return this.autocomplete(fullPath) + return this.autocompleteSchema() } const versionMatch = R_WITH_CAPI_VERSION.exec(fullPath) if (!versionMatch) { @@ -78,31 +79,36 @@ export abstract class CodegenServer { return this.redirect(request, `/@${defaultVersion}${fullPath}`) } const [, version, path] = versionMatch - if (version !== this.version) { - return this.delegateRequest(request, version!, path ?? "/") + const normalizedVersion = await this.normalizeVersion(version!) + if (normalizedVersion !== version) { + return this.redirect(request, `/@${normalizedVersion}${path}`) + } + if (!(await this.canHandleVersion(version!))) { + return this.delegateRequest(request, version!, path!) } if (!path) return this.redirect(request, `/@${version}/`) if (path === "/") { - return this.landingPage() + return this.landingPage(version!) } if (path.startsWith("/proxy/")) { const match = R_WITH_CHAIN_URL.exec(path) if (!match) return this.e404() const [, chainUrl, chainVersion, file] = match - return this.chainFile(request, chainUrl!, chainVersion, file!) + return this.chainFile(request, version!, chainUrl!, chainVersion, file!) } if (path.startsWith("/autocomplete/")) { - return this.autocomplete(path) + return this.autocompleteApi(path, version!) } - return this.moduleFile(request, path) + return this.moduleFile(request, version!, path) } - landingPage() { - return this.html(`
capi@${this.version}
`) + landingPage(version: string) { + return this.html(`
capi@${version}
`) } async chainFile( request: Request, + version: string, chainUrl: string, chainVersion: string | undefined, filePath: string, @@ -111,23 +117,21 @@ export abstract class CodegenServer { const latestChainVersion = await this.latestChainVersion(chainUrl) return this.redirect( request, - `/@${this.version}/proxy/${chainUrl}/@${latestChainVersion}/${filePath}`, + `/@${version}/proxy/${chainUrl}/@${latestChainVersion}/${filePath}`, ) } if (chainVersion !== this.normalizeChainVersion(chainVersion)) { return this.redirect( request, - `/@${this.version}/proxy/${chainUrl}/@${ - this.normalizeChainVersion(chainVersion) - }/${filePath}`, + `/@${version}/proxy/${chainUrl}/@${this.normalizeChainVersion(chainVersion)}/${filePath}`, ) } return this.ts(request, () => this.cache.getString( - `generated/@${this.version}/${chainUrl}/@${chainVersion}/${filePath}`, + `generated/@${version}/${chainUrl}/@${chainVersion}/${filePath}`, CHAIN_FILE_TTL, async () => { - const files = await this.files(chainUrl, chainVersion) + const files = await this.files(chainUrl, version, chainVersion) const content = files.getFormatted(filePath) if (content == null) throw this.e404() return content @@ -136,68 +140,69 @@ export abstract class CodegenServer { } filesMemo = new Map() - async files(chainUrl: string, chainVersion: string) { + async files(chainUrl: string, version: string, chainVersion: string) { const metadata = await this.metadata(chainUrl, chainVersion) return U.getOrInit(this.filesMemo, metadata, () => { return codegen({ metadata, clientDecl: chainUrl.startsWith("dev:") ? ` -import { LocalClientEffect } from ${JSON.stringify(`/@${this.version}/test_util/local.ts`)} +import { LocalClientEffect } from ${JSON.stringify(`/@${version}/test_util/local.ts`)} export const client = new LocalClientEffect(${JSON.stringify(chainUrl.slice(4))}) ` : ` -import * as C from ${JSON.stringify(`/@${this.version}/mod.ts`)} +import * as C from ${JSON.stringify(`/@${version}/mod.ts`)} export const client = C.rpc.rpcClient(C.rpc.proxyProvider, ${JSON.stringify(chainUrl)}) `, - importSpecifier: `/@${this.version}/mod.ts`, + importSpecifier: `/@${version}/mod.ts`, }) }) } - async chainIndex(chainUrl: string, chainVersion: string) { + async chainIndex(chainUrl: string, version: string, chainVersion: string) { return await this.cache.get( - `generated/@${this.version}/${chainUrl}/@${chainVersion}/_index`, + `generated/@${version}/${chainUrl}/@${chainVersion}/_index`, $index, async () => { - const files = await this.files(chainUrl, chainVersion) + const files = await this.files(chainUrl, version, chainVersion) return [...files.keys()] }, ) } - async autocomplete(path: string) { - if (path === "/.well-known/deno-import-intellisense.json") { - return this.json({ - version: 2, - registries: [ - { - schema: "/:version(@[^/]*)?/:file*", - variables: [ - { key: "version", url: "/autocomplete/version" }, - { key: "file", url: "/${version}/autocomplete/moduleFile/${file}" }, - ], - }, - { - schema: - "/:version(@[^/]*)/:_proxy(proxy)/:chainUrl(dev:\\w*|wss?:[^/]*)/:chainVersion(@[^/]+)/:file*", - variables: [ - { key: "version", url: "/autocomplete/version" }, - { key: "_proxy", url: "/autocomplete/null" }, - { key: "chainUrl", url: "/${version}/autocomplete/chainUrl/${chainUrl}" }, - { - key: "chainVersion", - url: "/${version}/autocomplete/chainVersion/${chainUrl}/${chainVersion}", - }, - { - key: "file", - url: "/${version}/autocomplete/chainFile/${chainUrl}/${chainVersion}/${file}", - }, - ], - }, - ], - }) - } + async autocompleteSchema() { + return this.json({ + version: 2, + registries: [ + { + schema: "/:version(@[^/]*)?/:file*", + variables: [ + { key: "version", url: "/autocomplete/version" }, + { key: "file", url: "/${version}/autocomplete/moduleFile/${file}" }, + ], + }, + { + schema: + "/:version(@[^/]*)/:_proxy(proxy)/:chainUrl(dev:\\w*|wss?:[^/]*)/:chainVersion(@[^/]+)/:file*", + variables: [ + { key: "version", url: "/autocomplete/version" }, + { key: "_proxy", url: "/autocomplete/null" }, + { key: "chainUrl", url: "/${version}/autocomplete/chainUrl/${chainUrl}" }, + { + key: "chainVersion", + url: "/${version}/autocomplete/chainVersion/${chainUrl}/${chainVersion}", + }, + { + key: "file", + url: "/${version}/autocomplete/chainFile/${chainUrl}/${chainVersion}/${file}", + }, + ], + }, + ], + }) + } + + async autocompleteApi(path: string, version: string) { const parts = path.slice(1).split("/") if (parts[0] !== "autocomplete") return this.e404() if (parts[1] === "null") { @@ -237,7 +242,7 @@ export const client = C.rpc.rpcClient(C.rpc.proxyProvider, ${JSON.stringify(chai if (parts[1] === "chainFile") { const chainUrl = parts[2]! const chainVersion = parts[3]?.slice(1) || await this.latestChainVersion(chainUrl) - const index = await this.chainIndex(chainUrl, chainVersion) + const index = await this.chainIndex(chainUrl, version, chainVersion) return this.json(this.autocompleteIndex(index, parts.slice(4))) } return this.e404() @@ -293,7 +298,7 @@ export const client = C.rpc.rpcClient(C.rpc.proxyProvider, ${JSON.stringify(chai }) } const html = await this.cache.getString( - `rendered/${this.version}/${path}.html`, + `rendered/${this.mainVersion}/${path}.html`, RENDERED_HTML_TTL, async () => { const content = await body() @@ -445,16 +450,12 @@ export const client = C.rpc.rpcClient(C.rpc.proxyProvider, ${JSON.stringify(chai return version } - async moduleFile(request: Request, path: string) { - return this.redirect(request, this.moduleFileUrl(this.version, path)) + async moduleFile(request: Request, version: string, path: string) { + return this.redirect(request, await this.moduleFileUrl(version, path)) } async delegateRequest(request: Request, version: string, path: string): Promise { - const normalizedVersion = await this.normalizeVersion(version) - if (normalizedVersion !== version) { - return this.redirect(request, `/@${normalizedVersion}${path}`) - } const url = await this.deploymentUrl(version) - return await fetch(new URL(new URL(request.url).pathname, url), { headers: request.headers }) + return await fetch(new URL(`/@${version}${path}`, url), { headers: request.headers }) } }