#!/usr/bin/env deno --allow-write --allow-read --allow-net import * as path from "./vendor/https/deno.land/std/fs/path.ts"; import * as fs from "./vendor/https/deno.land/std/fs/mod.ts"; import * as flags from "./vendor/https/deno.land/std/flags/mod.ts"; import { green, gray, red } from "./vendor/https/deno.land/std/fmt/colors.ts"; import { Modules } from "./mod.ts"; function isDinkModules(x, errors: string[]): x is Modules { if (typeof x !== "object") { errors.push("is not object"); return false; } for (const k in x) { if (x.hasOwnProperty(k)) { const m = x[k]; if (typeof m["version"] !== "string") { errors.push('"version" must be string'); return false; } if (!Array.isArray(m["modules"])) { errors.push(`\"modules\" must be array`); return false; } for (const mod of m["modules"]) { if (typeof mod !== "string") { errors.push(`"content of "modules" must be string`); return false; } } } } return true; } async function deleteRemovedFiles(modules: Modules, lockFile: Modules) { const removedFiles: string[] = []; for (const [k, v] of Object.entries(lockFile)) { const url = new URL(k); const { protocol, hostname, pathname } = url; const scheme = protocol.slice(0, protocol.length - 1); const dir = path.join("./vendor", scheme, hostname, pathname); if (!modules[k]) { for (const i of v.modules) { removedFiles.push(path.join(dir, i)); } } else { const mod = modules[k]; const set = new Set<string>(v.modules); mod.modules.forEach(i => set.delete(i)); for (const i of set.values()) { removedFiles.push(path.join(dir, i)); } } } await Promise.all( removedFiles.map(async i => { if (!(await fs.exists(i))) { return; } await Deno.remove(i); let dir = path.dirname(i); while ((await Deno.readDir(dir)).length === 0) { await Deno.remove(dir); dir = path.dirname(dir); } console.log(`${red("Removed")}: ./${i}`); }) ); } async function ensure(modules: Modules) { const encoder = new TextEncoder(); const lockFile = await readLockFile(); await deleteRemovedFiles(modules, lockFile); for (const [k, v] of Object.entries(modules)) { const url = new URL(k); const { protocol, hostname, pathname } = url; const scheme = protocol.slice(0, protocol.length - 1); const dir = path.join("./vendor", scheme, hostname, pathname); const writeLinkFile = async (mod: string) => { const modFile = path.join(dir, mod); const modDir = path.dirname(modFile); let lockedVersion = v.version; if (lockFile && lockFile[k] && lockFile[k].version) { lockedVersion = lockFile[k].version; } const specifier = `${k}${v.version}${mod}`; const hasLink = await fs.exists(modFile); if (hasLink && v.version === lockedVersion) { console.log(gray(`Linked: ${specifier} -> ./${modFile}`)); return; } const resp = await fetch(specifier, { method: "HEAD" }); if (resp.status !== 200) { throw new Error(`failed to fetch metadata for ${specifier}`); } const link = `export * from "${resp.url}";\n`; await Deno.mkdir(modDir, true); const f = await Deno.open(modFile, "w"); try { await Deno.write(f.rid, encoder.encode(link)); } finally { f.close(); } console.log(`${green("Linked")}: ${specifier} -> ./${modFile}`); }; await Promise.all(v.modules.map(writeLinkFile)); await generateLockFile(modules); } } async function generateLockFile(modules: Modules) { const obj = new TextEncoder().encode(JSON.stringify(modules, null, " ")); await Deno.writeFile("./modules-lock.json", obj); } async function readLockFile(): Promise<Modules | undefined> { if (await fs.exists("./modules-lock.json")) { const f = await Deno.readFile("./modules-lock.json"); const lock = JSON.parse(new TextDecoder().decode(f)); const err = []; if (!isDinkModules(lock, err)) { throw new Error( "lock file may be saved as invalid format: " + err.join(",") ); } return lock; } } const VERSION = "0.5.1"; type DinkOptions = { file?: string; }; async function main() { const args = flags.parse(Deno.args, { alias: { h: "help", V: "ver" }, "--": true }); if (args["V"] || args["ver"]) { console.log(VERSION); Deno.exit(0); } if (args["h"] || args["help"]) { console.log( String(` USAGE dink -A or dink --allow-write --allow-read ARGUMENTS OPTIONS -f, --file Custom path for module.json (Optional) GLOBAL OPTIONS -h, --help Display help -V, --ver Display version `) ); Deno.exit(0); } const opts: DinkOptions = { file: "./modules.json" }; if (args["f"]) { opts.file = args["f"]; } if (!(await fs.exists(opts.file))) { console.error(`${opts.file} does not exists`); Deno.exit(1); } const file = await Deno.readFile(opts.file); const decoder = new TextDecoder(); const json = JSON.parse(decoder.decode(file)) as Modules; const errors = []; if (!isDinkModules(json, errors)) { console.error(`${opts.file} has syntax error: ${errors.join(",")}`); Deno.exit(1); } await ensure(json); Deno.exit(0); } main();