import { colors, dirname, expandGlob, parse, Spinner } from "../deps.ts"; import type { Package } from "../registry/utils.ts"; import { registries } from "../registries.ts"; import { getImportMapFile } from "../import_map.ts"; export interface UpdateOptions { /** don't permanently edit files */ dryRun?: boolean; /** search in global scripts */ global?: boolean; /** Minimum age in hours */ minimumAge?: number; /** Ignored packages */ ignore?: string[]; } export default async function run(files: string[], options: UpdateOptions) { const ignored: string[] = []; const age = new Date(); age.setTime(age.getTime() - ((options.minimumAge ?? 0) * 1000 * 60 * 60)); const minimumAge = age.toISOString(); if (files.length === 0) { if (options.global) { // Get deno binary path const denoBin = Deno.execPath(); ignored.push(denoBin); files.push(dirname(denoBin) + "/*"); } else { files.push(await getImportMapFile()); } } if (files.length === 0) { console.error("No files found."); return; } const spinner = new Spinner({ message: "Scanning files..." }); spinner.start(); const depFiles: string[] = []; for (const arg of files.map((x) => x.toString())) { console.log(arg); for await (const file of expandGlob(arg)) { if (ignored.includes(file.path)) { continue; } depFiles.push(file.path); } } if (depFiles.length === 0) { spinner.stop(); console.error("No files found."); return; } if (depFiles.length === 1) { spinner.message = `Updating dependencies of ${depFiles[0]}...`; } else { spinner.message = `Updating dependencies of ${depFiles.length} files...`; } const results: UpdateResult[] = []; await Promise.all(depFiles.map(async (filename) => { results.push(...await update(filename, options, minimumAge)); })); spinner.stop(); const alreadyLatest = results.filter((x) => x.newVersion === undefined); if (alreadyLatest.length > 0) { console.log(colors.bold("\nAlready latest version:")); for (const a of alreadyLatest) { console.log(colors.dim(a.initUrl), "==", a.initVersion); } } const updated = results.filter((x) => x.newVersion !== undefined); if (updated.length > 0) { console.log( colors.bold( options.dryRun ? "\nAble to update:" : "\nSuccessfully updated:", ), ); for (const s of updated) { console.log(colors.green(s.initUrl), s.initVersion, "->", s.newVersion); } } } export interface UpdateResult { initUrl: string; initVersion: string; newVersion?: string; } export async function update( filename: string, options: UpdateOptions, minimumAge?: string, ): Promise { const results: UpdateResult[] = []; const content = Deno.readTextFileSync(filename); if (filename.endsWith(".json")) { const map = JSON.parse(content) as ImportMap; const updatedMap = await updateImportMap(map, options, results, minimumAge); if (updatedMap) { Deno.writeTextFileSync( filename, JSON.stringify(updatedMap, null, 2) + "\n", ); } return results; } const updatedContent = await updateCode( content, options, results, minimumAge, ); if (updatedContent) { Deno.writeTextFileSync(filename, updatedContent); } return results; } async function updateCode( content: string, options: UpdateOptions, results: UpdateResult[], minimumAge?: string, ): Promise { let changed = false; const packages = codeUrls(content); for (const [initUrl, pkg] of packages) { const initVersion = pkg.version; if (options.ignore?.includes(pkg.name)) { continue; } try { parse(initVersion); } catch { // The version string is a non-semver string like a branch name. results.push({ initUrl, initVersion }); continue; } const newVersion = await pkg.latestVersion(minimumAge); if (initVersion === newVersion) { results.push({ initUrl, initVersion }); continue; } if (!options.dryRun) { const newUrl = pkg.at(newVersion); content = content.replaceAll(initUrl, newUrl); changed = true; } results.push({ initUrl, initVersion, newVersion, }); } return changed ? content : undefined; } async function updateImportUrl( initUrl: string, options: UpdateOptions, results: UpdateResult[], minimumAge?: string, ): Promise { for (const R of registries) { if (R.regexp.some((r) => r.test(initUrl))) { const v = R.parse(initUrl); if (options.ignore?.includes(v.name)) { break; } const newVersion = await v.latestVersion(minimumAge); if (v.version !== newVersion && !options.dryRun) { results.push({ initUrl, initVersion: v.version, newVersion }); return v.at(newVersion); } results.push({ initUrl, initVersion: v.version }); break; } } return undefined; } interface Task { command: string; [key: string]: unknown; } interface Lint { plugins?: string[]; [key: string]: unknown; } interface CompilerOptions { types?: string[]; [key: string]: unknown; } interface ImportMap { imports?: Record; // only in deno.json tasks?: Record; lint?: Lint; compilerOptions?: CompilerOptions; } async function updateImportMap( json: ImportMap, options: UpdateOptions, results: UpdateResult[], minimumAge?: string, ): Promise { let changed = false; if ( !json.imports && !json.tasks && !json.lint?.plugins && !json.compilerOptions?.types ) { return; } if (json.imports) { for (const [key, initUrl] of Object.entries(json.imports)) { const updatedUrl = await updateImportUrl( initUrl, options, results, minimumAge, ); if (updatedUrl) { json.imports[key] = updatedUrl; changed = true; } } } if (json.tasks) { for (const [key, command] of Object.entries(json.tasks)) { const updatedCommand = typeof command === "string" ? await updateCode(command + " ", options, results, minimumAge) : await updateCode(command.command + " ", options, results, minimumAge); if (updatedCommand) { json.tasks[key] = typeof command === "string" ? updatedCommand.slice(0, -1) : { ...command, command: updatedCommand.slice(0, -1) }; changed = true; } } } if (json.lint?.plugins) { for (const [index, initUrl] of json.lint.plugins.entries()) { const updatedUrl = await updateImportUrl( initUrl, options, results, minimumAge, ); if (updatedUrl) { json.lint.plugins[index] = updatedUrl; changed = true; } } } if (json.compilerOptions?.types) { for (const [index, initUrl] of json.compilerOptions?.types.entries()) { const updatedUrl = await updateImportUrl( initUrl, options, results, minimumAge, ); if (updatedUrl) { json.compilerOptions.types[index] = updatedUrl; changed = true; } } } return changed ? json : undefined; } function codeUrls(content: string): Map { const packages: Map = new Map(); for (const R of registries) { const allRegexp = R.regexp.map((r) => new RegExp(`['"\\s]${r.source}['"\\s$]`, "g") ); for (const regexp of allRegexp) { const match = content.match(regexp); match?.forEach((url) => { const cleanUrl = url .replace(/^[^'"\s]*['"\s]+/g, "") .replace(/['"\s]+[^'"\s]*/g, ""); packages.set(cleanUrl, R.parse(cleanUrl)); content = content.replaceAll(url, ""); }); } } return packages; }