pickit/mod.ts

228 lines
5.7 KiB
TypeScript
Raw Normal View History

2023-02-10 08:17:05 +00:00
import {
cacheDir,
copy,
2023-02-11 11:13:48 +00:00
globToRegExp,
2023-02-10 08:17:05 +00:00
GzDecoder,
join,
readerFromStreamReader,
Transform,
Untar,
} from "./deps.ts";
2023-02-11 11:13:48 +00:00
import type {
ConfigEntry,
GithubPickOptions,
PathLike,
PickConfig,
ReadableEntry,
ReadPredicate,
ReadStrategy,
} from "./mod.d.ts";
2023-02-10 08:17:05 +00:00
export const CACHE_DIR = join((await cacheDir()) ?? ".cache", "__pick_cache0");
2023-02-10 16:30:37 +00:00
await Deno.mkdir(CACHE_DIR).catch((_) => {});
2023-02-10 08:17:05 +00:00
2023-02-11 11:13:48 +00:00
/**
* Read config and write files
*/
export async function write(entires: PickConfig) {
const _entries: ReadableEntry[] = entires.map(asReadable$);
for (const entry of _entries) {
const output = entry.output;
await Deno.mkdir(output, { recursive: true });
for await (const reader of entry.read()) {
const file = await Deno.open(
join(output, filename(reader.fileName ?? "")),
{ create: true, write: true },
);
await copy(reader, file);
file.close();
}
}
}
/**
* Decorator which makes config entries readable
*/
function asReadable$(entry: ConfigEntry): ReadableEntry {
// convert globs to regex if required
const pick = entry.pick.map((p) => {
if (typeof p === "string") {
return globToRegExp(p);
}
return p;
});
// if read is a function, use it
let read: ReadableEntry["read"];
if (typeof entry.read === "function") {
read = () => (entry.read as ReadPredicate)(entry);
} else {
// otherwise, use a specific strategy
const strategy = entry.strategy ?? autoStrategy$(entry.source);
switch (strategy) {
case "tar": {
read = () => tarPickFiles(entry.source, pick);
break;
}
case "targz": {
read = () => tarGzPickFiles(entry.source, pick);
break;
}
case "github": {
2023-02-11 13:06:20 +00:00
const [repo, ...version] = entry.source.split("@");
2023-02-11 11:13:48 +00:00
if (!version) {
throw new Error(
2023-02-11 13:06:20 +00:00
`Invalid source format: ${entry.source}
2023-02-11 11:13:48 +00:00
Expected: <repo>@<version>`,
);
}
2023-02-11 13:25:32 +00:00
read = () => githubPick({ repo, version: version.join("@"), pick });
2023-02-11 11:13:48 +00:00
break;
}
default: {
throw new Error(`Unknown strategy: ${entry.strategy}`);
}
}
}
return {
...entry,
read,
};
}
function autoStrategy$(source: string): ReadStrategy {
if (source.endsWith(".tar")) {
return "tar";
} else if (source.endsWith(".tar.gz")) {
return "targz";
} else if (source.includes("@")) {
return "github";
} else {
throw new Error(`Unknown strategy for source: ${source}`);
}
}
function filename(path: string) {
return path.split("/").pop() ?? "";
2023-02-10 08:17:05 +00:00
}
2023-02-10 16:30:37 +00:00
/**
* Pick files from a github repo.
* @param {GithubPickOptions} opts - { repo: "denoland/deno", version: "v1.0.0", pick: [/\.ts$/] }
*/
2023-02-11 11:13:48 +00:00
export async function* githubPick(
{ repo, version, pick }: GithubPickOptions,
): ReturnType<ReadableEntry["read"]> {
2023-02-10 16:30:37 +00:00
await Deno.mkdir(join(CACHE_DIR, repo.split("/").shift() ?? "")).catch(
(_) => {},
);
2023-02-10 08:17:05 +00:00
const _cached = await getFetchCache(`${repo}@${version}`);
if (_cached) {
const reader = await Deno.open(_cached);
yield* tarGzPickFiles(reader, pick);
2023-02-10 16:30:37 +00:00
reader.close();
2023-02-10 08:17:05 +00:00
} else {
yield* githubPickFiles({ repo, version, pick });
}
}
/**
* Reads *.tar.gz file from a version tag and returns generator of the files that match the pick regex.
*
* @param {GithubPickOptions} opts - { repo: "denoland/deno", version: "v1.0.0", pick: [/\.ts$/] }
*/
export async function* githubPickFiles(
{ repo, version, pick }: GithubPickOptions,
2023-02-11 11:13:48 +00:00
): ReturnType<ReadableEntry["read"]> {
2023-02-10 08:17:05 +00:00
const targz = await fetch(
`https://github.com/${repo}/archive/refs/tags/${version}.tar.gz`,
);
2023-02-10 16:30:37 +00:00
const name = `${repo}@${version}`;
await putFetchCache(targz.body, name);
const file = await Deno.open(join(CACHE_DIR, name));
yield* tarGzPickFiles(file, pick);
file.close();
2023-02-10 08:17:05 +00:00
}
/**
* Pick files from a tar.gz archive.
*
2023-02-11 11:13:48 +00:00
* @param {Deno.Reader} targz - Deno.open('archive.tar.gz')
2023-02-10 08:17:05 +00:00
* @param {RegExp[]} pick - [ /\.ts$/ ]
*/
export async function* tarGzPickFiles(
2023-02-11 11:13:48 +00:00
targz: Deno.Reader | Deno.FsFile | PathLike,
2023-02-10 08:17:05 +00:00
pick: RegExp[],
2023-02-11 11:13:48 +00:00
): ReturnType<ReadableEntry["read"]> {
if (typeof targz === "string" || targz instanceof URL) {
targz = await Deno.open(targz);
}
2023-02-10 08:17:05 +00:00
const untar = new Untar(
Transform.newReader(
targz,
new GzDecoder(),
),
);
for await (const entry of untar) {
if (entry.type === "file" && pick.some((re) => re.test(entry.fileName))) {
yield entry;
}
}
}
2023-02-11 11:13:48 +00:00
/**
* Pick files from a tar.gz archive.
*
* @param {Deno.Reader} tar - Deno.open('archive.tar')
* @param {RegExp[]} pick - [ /\.ts$/ ]
*/
export async function* tarPickFiles(
tar: Deno.Reader | Deno.FsFile | PathLike,
pick: RegExp[],
): ReturnType<ReadableEntry["read"]> {
if (typeof tar === "string" || tar instanceof URL) {
tar = await Deno.open(tar);
}
const untar = new Untar(tar);
for await (const entry of untar) {
if (entry.type === "file" && pick.some((re) => re.test(entry.fileName))) {
yield entry;
}
}
}
2023-02-10 08:17:05 +00:00
/**
* Writes fetch body to a cache file.
*/
export async function putFetchCache(body: Response["body"], name: string) {
const reader = readerFromStreamReader(body!.getReader());
2023-02-11 11:13:48 +00:00
const _file = join(CACHE_DIR, name);
await Deno.remove(_file).catch((_) => {});
const writer = await Deno.open(_file, {
2023-02-10 16:30:37 +00:00
write: true,
create: true,
});
2023-02-10 08:17:05 +00:00
await copy(reader, writer);
writer.close();
}
/**
* Get either the path to the cache file or null if it doesn't exist.
*/
export async function getFetchCache(name: string) {
try {
const _file = join(CACHE_DIR, name);
await Deno.lstat(_file);
return _file;
} catch (_) {
return null;
}
}
/**
* Clean the fetch cache.
*/
export async function cleanFetchCache() {
2023-02-11 11:13:48 +00:00
return await Deno.remove(CACHE_DIR, { recursive: true });
2023-02-10 08:17:05 +00:00
}