import { existsSync } from "../deno_ral/fs.ts";
import { isWindows } from "../deno_ral/platform.ts";
export interface ResolvedExtensionInfo {
url: string;
urlFile?: string;
response: Promise<Response>;
subdirectory?: string;
owner?: string;
learnMoreUrl?: string;
}
export interface ExtensionSource {
type: "remote" | "local";
owner?: string;
resolvedTarget: Response | string;
resolvedFile?: string;
targetSubdir?: string;
learnMoreUrl?: string;
}
export async function extensionSource(
target: string,
): Promise<ExtensionSource | undefined> {
if (!target.match(/^https?:\/\/.*$/) && existsSync(target)) {
return { type: "local", resolvedTarget: target };
}
for (const resolver of extensionHostResolvers) {
const resolved = resolver(target);
if (!resolved) {
continue;
}
try {
const response = await resolved.response;
if (response.status === 200) {
return {
type: "remote",
resolvedTarget: response,
resolvedFile: resolved?.urlFile,
owner: resolved?.owner,
targetSubdir: resolved?.subdirectory,
learnMoreUrl: resolved?.learnMoreUrl,
};
}
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}
err.message =
`A network error occurred when attempting to inspect the extension '${target}'. Please try again.\n\n` +
err.message;
throw err;
}
}
return undefined;
}
type ExtensionNameResolver = (
name: string,
) => ResolvedExtensionInfo | undefined;
interface ExtensionHostSource {
parse(name: string): ExtensionHost | undefined;
urlProviders: ExtensionUrlProvider[];
}
interface ExtensionHost {
name: string;
organization: string;
repo: string;
modifier?: string;
subdirectory?: string;
}
interface ExtensionUrlProvider {
extensionUrl: (host: ExtensionHost) => string | undefined;
archiveSubdir: (host: ExtensionHost) => string | undefined;
learnMoreUrl: (host: ExtensionHost) => string | undefined;
}
const archiveExt = isWindows ? ".zip" : ".tar.gz";
const githubLatestUrlProvider = {
extensionUrl: (host: ExtensionHost) => {
if (host.modifier === undefined || host.modifier === "latest") {
return `https://github.com/${host.organization}/${host.repo}/archive/refs/heads/main${archiveExt}`;
}
},
archiveSubdir: (host: ExtensionHost) => {
const baseDir = `${host.repo}-main`;
if (host.subdirectory) {
return baseDir + "/" + host.subdirectory;
} else {
return baseDir;
}
},
learnMoreUrl: (host: ExtensionHost) => {
return `https://www.github.com/${host.organization}/${host.repo}#readme`;
},
};
const githubTagUrlProvider = {
extensionUrl: (host: ExtensionHost) => {
if (host.modifier) {
return `https://github.com/${host.organization}/${host.repo}/archive/refs/tags/${host.modifier}${archiveExt}`;
}
},
archiveSubdir: (host: ExtensionHost) => {
const baseDir = `${host.repo}-${tagSubDirectory(host.modifier)}`;
if (host.subdirectory) {
return baseDir + "/" + host.subdirectory;
} else {
return baseDir;
}
},
learnMoreUrl: (host: ExtensionHost) => {
return `https://github.com/${host.organization}/${host.repo}/tree/${host.modifier}#readme`;
},
};
const githubBranchUrlProvider = {
extensionUrl: (host: ExtensionHost) => {
if (host.modifier) {
return `https://github.com/${host.organization}/${host.repo}/archive/refs/heads/${host.modifier}${archiveExt}`;
}
},
archiveSubdir: (host: ExtensionHost) => {
const baseDir = `${host.repo}-${host.modifier}`;
if (host.subdirectory) {
return baseDir + "/" + host.subdirectory;
} else {
return baseDir;
}
},
learnMoreUrl: (host: ExtensionHost) => {
return `https://github.com/${host.organization}/${host.repo}/tree/${host.modifier}#readme`;
},
};
const kGithubExtensionSource: ExtensionHostSource = {
parse: (name: string) => {
const match = name.match(kGithubExtensionNameRegex);
if (match) {
return {
name,
organization: match[1],
repo: match[2],
subdirectory: match[3],
modifier: match[4],
};
} else {
return undefined;
}
},
urlProviders: [
githubLatestUrlProvider,
githubTagUrlProvider,
githubBranchUrlProvider,
],
};
const kGithubExtensionNameRegex =
/^([a-zA-Z0-9-_\.]*?)\/([a-zA-Z0-9-_\.]*?)(?:\/(.*?)\/?)?(?:@([a-zA-Z0-9-_\.\/]*))?$/;
const kGithubArchiveUrlSource: ExtensionHostSource = {
parse: (name: string) => {
const match = name.match(kGithubArchiveUrlRegex);
if (match) {
return {
name,
organization: match[1],
repo: match[2],
subdirectory: undefined,
modifier: match[3],
};
} else {
return undefined;
}
},
urlProviders: [
{
extensionUrl: (host: ExtensionHost) => host.name,
archiveSubdir: (host: ExtensionHost) => {
return `${host.repo}-${tagSubDirectory(host.modifier)}`;
},
learnMoreUrl: (host: ExtensionHost) =>
`https://www.github.com/${host.organization}/${host.repo}`,
},
],
};
const kGithubArchiveUrlRegex =
/https?\:\/\/github.com\/([a-zA-Z0-9-_\.]+?)\/([a-zA-Z0-9-_\.]+?)\/archive\/refs\/(?:tags|heads)\/(.+)(?:\.tar\.gz|\.zip)$/;
function tagSubDirectory(tag?: string) {
if (tag) {
return tag.startsWith("v") ? tag.slice(1) : tag;
} else {
return tag;
}
}
function makeResolvers(
source: ExtensionHostSource,
): ExtensionNameResolver[] {
return source.urlProviders.map((urlProvider) => {
return (name) => {
const host = source.parse(name);
if (!host) {
return undefined;
}
const url = urlProvider.extensionUrl(host);
if (url) {
return {
url,
urlFile: url.split("/").pop(),
response: fetch(url),
owner: host.organization,
subdirectory: urlProvider.archiveSubdir(host),
learnMoreUrl: urlProvider.learnMoreUrl(host),
};
} else {
return undefined;
}
};
});
}
const kGithubResolvers = [
...makeResolvers(kGithubExtensionSource),
...makeResolvers(kGithubArchiveUrlSource),
];
const unknownUrlResolver = (
name: string,
): ResolvedExtensionInfo | undefined => {
try {
new URL(name);
} catch {
return undefined;
}
return {
url: name,
response: fetch(name),
};
};
const extensionHostResolvers: ExtensionNameResolver[] = [
...kGithubResolvers,
unknownUrlResolver,
];