diff --git a/package-lock.json b/package-lock.json index b7c98ac..befbae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "taze": "^0.16.1", "ts-dedent": "^2.2.0", "ts-essentials": "^10.0.1", + "tsx": "^4.16.5", "typescript": "^5.5.3", "vitest": "^2.0.3" }, @@ -4391,6 +4392,16 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", @@ -4610,6 +4621,16 @@ "node": ">=0.8" } }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -12506,9 +12527,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz", - "integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==", + "version": "4.16.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.5.tgz", + "integrity": "sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==", "dev": true, "license": "MIT", "dependencies": { @@ -14574,7 +14595,11 @@ }, "devDependencies": { "ajv-cli": "^5.0.0", - "ajv-formats": "^3.0.1" + "ajv-formats": "^3.0.1", + "citty": "^0.1.6", + "consola": "^3.2.3", + "texlive-json-schemas": "^0.2.0", + "url-template": "^3.1.1" }, "peerDependencies": { "semver": "*" diff --git a/package.json b/package.json index d963ba6..3f96ea0 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "taze": "^0.16.1", "ts-dedent": "^2.2.0", "ts-essentials": "^10.0.1", + "tsx": "^4.16.5", "typescript": "^5.5.3", "vitest": "^2.0.3" }, diff --git a/packages/data/.gitattributes b/packages/data/.gitattributes new file mode 100644 index 0000000..b48e166 --- /dev/null +++ b/packages/data/.gitattributes @@ -0,0 +1 @@ +/data/package-names.json linguist-generated diff --git a/packages/data/data/package-names.json b/packages/data/data/package-names.json new file mode 100644 index 0000000..d384654 --- /dev/null +++ b/packages/data/data/package-names.json @@ -0,0 +1,540 @@ +{ + "toTL": { + "a4": "ntgclass", + "abbrevs": "frankenstein", + "abstbook": "ltxmisc", + "abstyles-babel": "abstyles", + "abstyles-orig": "abstyles", + "accenti": "bosisio", + "achicago": "frankenstein", + "achicago-bst": "frankenstein", + "afterpackage": "ncctools", + "afterpage": "tools", + "aliascnt": "oberdiek", + "alltt": "latex", + "alphanum": "jura", + "altverse": "shipunov", + "amsart": "amscls", + "amsbook": "amscls", + "amsbsy": "amsmath", + "amscd": "amsmath", + "amslatex": "amsmath", + "amslatexdoc-vietnamese": "amsldoc-vn", + "amsmidx": "amscls", + "amsopn": "amsmath", + "amsppt": "amstex", + "amsppt1": "amstex", + "amsproc": "amscls", + "amstext": "amsmath", + "amsthm": "amscls", + "annotation": "beebe", + "apa5": "apa", + "apabst": "beebe", + "aramaic": "archaic", + "array": "tools", + "arrayjob": "arrayjobx", + "arrow": "eplain", + "article": "latex", + "at": "mdwtools", + "attrib": "frankenstein", + "auncial": "bookhands", + "authblk": "preprint", + "autoinst": "fontools", + "autolist": "shipunov", + "backref": "hyperref", + "bahyph": "hyphen-basque", + "balance": "preprint", + "base": "plain", + "baskervaldadf": "baskervald", + "bbs": "beebe", + "bdfchess": "chess", + "beletter": "ltxmisc", + "bengali-pandey": "bengali", + "besjournals-bst": "besjournals", + "bibcheck": "ltxmisc", + "biblio": "beebe", + "bibtex8bit": "bibtex8", + "bicaption": "caption", + "bigdelim": "multirow", + "bigstrut": "multirow", + "binhex": "kastrup", + "biolist": "shipunov", + "blkcntrl": "frankenstein", + "bm": "tools", + "bmpsize": "oberdiek", + "boldline": "shipunov", + "book": "latex", + "boxedminipage2e": "boxedminipage", + "cahyph": "hyphen-spanish", + "calc": "tools", + "caption2": "caption", + "carolmin": "bookhands", + "carolmin-t1": "carolmin-ps", + "cassette-shipunov": "shipunov", + "catalan": "hyphen-spanish", + "cbe": "beebe", + "cbgreek-complete": "cbfonts", + "ccref": "crefthe", + "cdcover": "cd-cover", + "cdlabeler": "eijkhout", + "centernot": "oberdiek", + "cfgguide": "latex", + "chapterbib": "cite", + "chemarr": "oberdiek", + "chemscheme": "chemstyle", + "cite-bundle": "cite", + "cjk": "cjkutils", + "cjk-fonts": "cns", + "classes": "latex", + "classif2": "shipunov", + "classlist": "oberdiek", + "clsguide": "latex", + "cm-mf": "cm", + "cm-pk": "cm", + "cm-tfm": "cm", + "cmextra-latex": "latex", + "cmfrak": "gothic", + "cmtt": "mdwtools", + "cmyk-hax": "tex-ps", + "collect": "sauerj", + "colonequals": "oberdiek", + "color": "graphics", + "colordvi": "dvips", + "compsci": "frankenstein", + "concrete-macros": "ltxmisc", + "csfonts": "cs", + "csfonts-t1": "cs", + "csname-doc": "plain-doc", + "cspsfonts": "cslatex", + "ctib4tex": "ctib", + "cuted": "sttools", + "cvsty": "cv", + "cwebbin": "cweb", + "cypriot": "archaic", + "cyrguide": "latex", + "dblfnote": "yafoot", + "dblfont": "bosisio", + "dbprocess": "eijkhout", + "dcolumn": "tools", + "dcounter": "ncctools", + "delarray": "tools", + "desclist": "ncctools", + "devanagari": "velthuis", + "diagxy": "barr", + "dialogue": "frankenstein", + "dkhyphen": "hyphen-danish", + "doafter": "mdwtools", + "doc": "latex", + "docstrip": "latex", + "dotlessj": "carlisle", + "drcaps": "shipunov", + "drftcite": "cite", + "duplicat": "piff", + "dvibook": "seetexk", + "dviconcat": "seetexk", + "dviout": "dviout.windows", + "dvipscol": "oberdiek", + "dvipsk": "dvips", + "dvitype": "texware", + "e-tex": "etex", + "easybib": "easy", + "easybmat": "easy", + "easyeqn": "easy", + "easymat": "easy", + "easytable": "easy", + "easyvector": "easy", + "egothic": "bookhands", + "electrumadf": "electrum", + "eledpar": "eledmac", + "elhyphen": "hyphen-greek", + "engord": "oberdiek", + "enparen": "oberdiek", + "enumerate": "tools", + "envmath": "bosisio", + "eolgrab": "oberdiek", + "epsfig": "graphics", + "epsfx": "tex-ps", + "eptex": "ptex", + "etruscan": "archaic", + "euptex": "uptex", + "evenpage": "bosisio", + "expkv": "expkv-bundle", + "expkv-cs": "expkv-bundle", + "expkv-def": "expkv-bundle", + "expkv-opt": "expkv-bundle", + "expl3": "l3kernel", + "exscale": "latex", + "extdash": "ncctools", + "fancyheadings": "fancyhdr", + "faq-es": "es-tex-faq", + "fepslatex": "epslatex-fr", + "fibnum": "oberdiek", + "fifinddo": "nicetext", + "figcaps": "preprint", + "fihyph": "hyphen-finnish", + "fileerr": "tools", + "findfont": "luafindfont", + "findpkg": "texfindpkg", + "finplain": "finbib", + "fix-cm": "latex", + "fixltx2e": "latex", + "flags": "oberdiek", + "flashcard": "ltxmisc", + "floatpag": "sttools", + "flushend": "sttools", + "fnlineno": "lineno", + "fnpos": "yafoot", + "fntguide": "latex", + "fontchart": "plain", + "fontenc": "latex", + "fontsmpl": "tools", + "footnote": "mdwtools", + "ftnright": "tools", + "fullpage": "preprint", + "gentium": "gentium-tug", + "geometry-de": "geometry", + "ghostscript": "tlgs.windows", + "ghyphen": "dehyph", + "gkpmac": "plain", + "glhyph": "hyphen-spanish", + "glossaries-accsupp": "glossaries", + "gnhyph": "dehyph", + "graphicx": "graphics", + "graphpap": "latex", + "greek-makeindex": "mkgrkindex", + "greek4cbc": "archaic", + "greek6cbc": "archaic", + "greekctr": "jknapltx", + "grfguide": "graphics", + "helvet": "psnfss", + "hhline": "tools", + "hieroglf": "archaic", + "hint": "hitex", + "holtpolt": "jknapltx", + "holtxdoc": "oberdiek", + "hrhyph": "hyphen-croatian", + "humanbio": "beebe", + "humanist": "bookhands", + "humannat": "beebe", + "huncial": "bookhands", + "hungarian": "hyphen-hungarian", + "hypbmsec": "oberdiek", + "hypgotoe": "oberdiek", + "hyphsubst": "oberdiek", + "icehyph": "hyphen-icelandic", + "icomma": "was", + "ieeetrantools": "ieeetran", + "ifdraft": "oberdiek", + "ifetex": "iftex", + "iflang": "oberdiek", + "ifluatex": "iftex", + "ifpdf": "iftex", + "ifthen": "latex", + "ifvtex": "iftex", + "ifxetex": "iftex", + "impatient": [ + "impatient-cn", + "impatient-fr" + ], + "indentfirst": "tools", + "inputenc": "latex", + "ithyph": "hyphen-italian", + "japanese-otf-uptex": "japanese-otf", + "jknappen": "jknapltx", + "keyval": "graphics", + "knuth-letter": "plain", + "knuth-local": "cmextra", + "koma-script-examples-5": "koma-script-examples", + "ksfh-nat": "ksfh_nat", + "l3keys2e": "l3packages", + "lahyph": "hyphen-latin", + "latex-amsmath": "amsmath", + "latex-base": "latex", + "latex-cyrillic": "cyrillic", + "latex-doc": "latex", + "latex-firstaid": "firstaid", + "latex-graphics": "graphics", + "latex-tools": "tools", + "latex2e-help-texinfo": "latex2e-help-texinfo-spanish", + "latin1jk": "jknapltx", + "latin2jk": "jknapltx", + "latin3jk": "jknapltx", + "lato-math": "lete-sans-math", + "lcdf-typetools": "lcdftypetools", + "ledarab": "ledmac", + "ledpar": "ledmac", + "letter": "latex", + "letterformat": "plain", + "levy-font": "levy", + "lgc-examples": "latex-graphics-companion", + "linearb": "archaic", + "linsys": "ltxmisc", + "lips": "frankenstein", + "lithuanian-babel": "babel-lithuanian", + "llmk": "light-latex-make", + "lmodern": "lm", + "logsys": "coordsys", + "longtable": "tools", + "lscape": "graphics", + "lshort-zh-cn": "lshort-chinese", + "ltugboat": "tugboat", + "ltxdoc": "latex", + "lwc-examples": "latex-web-companion", + "makedoc": "nicetext", + "makeidx": "latex", + "makeindexk": "makeindex", + "manual": "manfnt-font", + "manyfoot": "ncctools", + "mathalfa": "mathalpha", + "mathbbol": "jknapltx", + "mathptmx": "psnfss", + "mathrsfs": "jknapltx", + "mboxfill": "ncctools", + "mdwlist": "mdwtools", + "mdwmath": "mdwtools", + "mdwtab": "mdwtools", + "memhfixc": "memoir", + "metainfo": "sauerj", + "mf": "metafont", + "mf-ps": "roex", + "minimal": "latex", + "mirr": "tex-ps", + "mltex-ltx": "mltex", + "modguide": "latex", + "moredefs": "frankenstein", + "mp": "metapost", + "mpost": "metapost", + "multicol": "tools", + "myfilist": "fileinfo", + "nameref": "hyperref", + "namunsrt": "beebe", + "natmove": "achemso", + "nbaseprt": "numprint", + "nccbbb": "ncctools", + "nccboxes": "ncctools", + "ncccomma": "ncctools", + "ncccropbox": "ncctools", + "ncccropmark": "ncctools", + "nccfancyhdr": "ncctools", + "nccfloats": "ncctools", + "nccfoots": "ncctools", + "nccmath": "ncctools", + "nccparskip": "ncctools", + "nccpic": "ncctools", + "nccrules": "ncctools", + "nccsect": "ncctools", + "nccstretch": "ncctools", + "nccthm": "ncctools", + "nehyph": "hyphen-dutch", + "neo-euler": "euler-math", + "newclude": "frankenstein", + "newcm": "newcomputermodern", + "newproof": "piff", + "ngerman": "german", + "nicefrac": "units", + "niceverb": "nicetext", + "nohyph": "hyphen-norwegian", + "nohyphbx": "hyphen-norwegian", + "oands": "archaic", + "oldprsn": "archaic", + "omega": "omegaware", + "one2many": "12many", + "onepagem": "piff", + "optparams": "sauerj", + "overcite": "cite", + "parboxx": "jknapltx", + "parcolumns": "sauerj", + "pdfcolparallel": "oberdiek", + "pdfcolparcolumns": "oberdiek", + "pdfcrypt": "oberdiek", + "perpage": "bigfoot", + "pfnote": "yafoot", + "pgfkeys": "pgf", + "pgfplotstable": "pgfplots", + "pgothic": "bookhands", + "phoenician": "archaic", + "picmac": "plain", + "pictex-autoarea": "autoarea", + "pictexwd": "pictex", + "pifont": "psnfss", + "pl": "plweb", + "pl-mf": "pl", + "plfonts": "pl", + "plhyph": "hyphen-polish", + "plpsfont": "pl", + "poligraf": "tex-ps", + "polishlipsum": "bredzenie", + "proc": "latex", + "processkv": "sauerj", + "protecteddef": "oberdiek", + "protosem": "archaic", + "psfont": "altfont", + "psmerge": "psutils", + "pst-xkey": "xkeyval", + "pstricks-base": "pstricks", + "pstricks-calcnotes": "pstricks_calcnotes", + "r-und-s": "r_und_s", + "ratex": "rtklage", + "readprov": "fileinfo", + "refer": "beebe", + "repeat": "eijkhout", + "report": "latex", + "resizegather": "oberdiek", + "revtex4-0": "revtex4", + "romandeadf": "romande", + "rotchiffre": "oberdiek", + "rubikcube": "rubik", + "rubikrotation": "rubik", + "rubiktwocube": "rubik", + "rule-d": "gs1", + "runic": "archaic", + "sans": "jknapltx", + "sarabian": "archaic", + "scalefnt": "carlisle", + "schwell": "gothic", + "scraddr": "koma-script", + "scrartcl": "koma-script", + "scrarticle": "koma-script", + "scrbase": "koma-script", + "scrbook": "koma-script", + "scrdate": "koma-script", + "scrextend": "koma-script", + "scrindex": "oberdiek", + "scrjura": "koma-script", + "scrlayer": "koma-script", + "scrlayer-notecolumn": "koma-script", + "scrlayer-scrpage": "koma-script", + "scrletter": "koma-script", + "scrlfile": "koma-script", + "scrlttr2": "koma-script", + "scrreport": "koma-script", + "scrreprt": "koma-script", + "scrtime": "koma-script", + "semtrans": "jknapltx", + "serbian-book": "srbook-mem", + "setouterhbox": "oberdiek", + "settobox": "oberdiek", + "sgmlcmpt": "jknapltx", + "shellesc": "tools", + "shortvrb": "latex", + "showframe": "eso-pic", + "showkeys": "tools", + "slashed": "carlisle", + "slemph": "frankenstein", + "slides": "latex", + "smartmn": "jknapltx", + "soulutf8": "soul", + "source2e": "latex", + "spanish": "babel-spanish", + "sqrcaps": "bookhands", + "srhyphc": "hyphen-serbian", + "stackrel": "oberdiek", + "stampinclude": "oberdiek", + "startlatex2e": "yet-another-guide-latex2e", + "structuredlog": "latex", + "subcaption": "caption", + "sueterlin": "gothic", + "suffix": "bigfoot", + "sverb": "mdwtools", + "sympytex": "sympytexpackage", + "syntax-mdw": "mdwtools", + "syntax2": "syntax", + "syntonly": "latex", + "t1enc": "latex", + "t2": "cyrplain", + "tabto": "tabto-ltx", + "tabularht": "oberdiek", + "tabularkv": "oberdiek", + "tabularx": "tools", + "tangle": "web", + "tccompat": "jknapltx", + "tclldoc": "tcldoc", + "testflow": "ieeetran", + "tex-gyre-adventor": "tex-gyre", + "tex-gyre-bonum": "tex-gyre", + "tex-gyre-chorus": "tex-gyre", + "tex-gyre-cursor": "tex-gyre", + "tex-gyre-heros": "tex-gyre", + "tex-gyre-math-pagella": "tex-gyre-math", + "tex-gyre-math-termes": "tex-gyre-math", + "tex-gyre-pagella": "tex-gyre", + "tex-gyre-schola": "tex-gyre", + "tex-gyre-termes": "tex-gyre", + "tex-references": "tex-refs", + "texinfo-latest": "texinfo", + "tgothic": "bookhands", + "theorem": "tools", + "thepdfnumber": "oberdiek", + "thrmappendix": "ltxmisc", + "tikz": "pgf", + "time": "piff", + "titleps": "titlesec", + "titles": "frankenstein", + "titletoc": "titlesec", + "tkhyph": "hyphen-turkish", + "tlc2-examples": "tlc2", + "tocbasic": "koma-script", + "tocenter": "ncctools", + "topcapt": "ltxmisc", + "topsection": "ncctools", + "trace": "tools", + "trans": "tex-ps", + "treetex-plain": "treetex", + "trig": "graphics", + "twoopt": "oberdiek", + "twoup-gen": "2up", + "twoupltx": "twoup", + "type1ec": "cm-super", + "typearea": "koma-script", + "ugarite": "archaic", + "uncial": "bookhands", + "upgreek": "was", + "upref": "amscls", + "urw-antiqua": "antiqua", + "urw-base35": [ + "avantgar", + "bookman", + "courier", + "helvetic", + "ncntrsbk", + "palatino", + "symbol", + "times", + "zapfchan", + "zapfding" + ], + "urw-grotesq": "grotesq", + "usrguide": "latex", + "varioref": "tools", + "verbatim": "tools", + "vfware": "fontware", + "viking": "archaic", + "vowel": "tipa", + "vrbexin": "ltxmisc", + "wasy2": "wasy", + "wasy2-ps": "wasy-type1", + "watermark": "ncctools", + "weave": "web", + "wiki": "nicetext", + "wncyr": "amsfonts", + "xdvik": "xdvi", + "xdvipdfmx": "dvipdfmx", + "xfp": "l3packages", + "xintexpr": "xint", + "xkvltxp": "xkeyval", + "xkvview": "xkeyval", + "xparse": "l3packages", + "xput": "pagelayout", + "xr": "tools", + "xr-hyper": "hyperref", + "xspace": "tools", + "xtemplate": "l3packages", + "yfrak": "gothic", + "ygoth": "gothic", + "yinit": "gothic", + "young": "jknapltx", + "yswab": "gothic", + "zahl2string": "sauerj" + }, + "generated": "2024-08-09T09:31:02.650Z" +} diff --git a/packages/data/package.json b/packages/data/package.json index 40b719b..e4f1e73 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { + "build": "tsx scripts/generate-namemap.ts -o data/package-names.json", "test": "task" }, "exports": { @@ -19,6 +20,10 @@ }, "devDependencies": { "ajv-cli": "^5.0.0", - "ajv-formats": "^3.0.1" + "ajv-formats": "^3.0.1", + "citty": "^0.1.6", + "consola": "^3.2.3", + "texlive-json-schemas": "^0.2.0", + "url-template": "^3.1.1" } } diff --git a/packages/data/schemas/package-names.schema.json b/packages/data/schemas/package-names.schema.json new file mode 100644 index 0000000..76eb566 --- /dev/null +++ b/packages/data/schemas/package-names.schema.json @@ -0,0 +1,27 @@ +{ + "$id": "package-names.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "toTL": { + "description": "Package-name mapping from CTAN to TeX Live", + "type": "object", + "patternProperties": { + "^\\S*$": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + } + }, + "additionalProperties": false + }, + "generated": { + "description": "Approximate date and time of generation", + "type": "string", + "format": "date-time" + } + }, + "required": ["toTL"], + "additionalProperties": false +} diff --git a/packages/data/scripts/generate-namemap.ts b/packages/data/scripts/generate-namemap.ts new file mode 100644 index 0000000..cdc739e --- /dev/null +++ b/packages/data/scripts/generate-namemap.ts @@ -0,0 +1,286 @@ +#!/usr/bin/env -S npx tsx +/** + * @packageDocumentation + * + * Some packages have different names in CTAN and TeX Live, and + * there is no one-to-one correspondence. For example, + * + * - There is an entry for [`xparse`](https://www.ctan.org/pkg/xparse) in CTAN, + * but in TeX Live it is distributed as part of `l3packages` bundle. + * + * - [`tikz`](https://www.ctan.org/pkg/tikz) is an alias of `pgf` in CTAN, + * but only `pgf` is the valid name in TeX Live. + * + * So, to use these packages, we need to do + * + * ```console + * $ tlmgr install l3packages pgf + * ``` + * + * rather than + * + * ```console + * $ tlmgr install xparse tikz + * ``` + * + * This script generates a name map from CTAN to TL + * to find the appropriate package names for installation. + * The relationship is retrieved from the following data: + * + * TL to CTAN: + * `catalogue` and `cataloguedata.alias` properties in + * [TLPDB](https://tug.org/svn/texlive/trunk/Master/tlpkg/doc/json-formats.txt). + * + * CTAN to TL: + * `texlive` property of the + * [CTAN API](https://www.ctan.org/help/json/2.0/pkg). + */ +import { execFileSync } from 'node:child_process'; +import { mkdir, open, writeFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import { setTimeout } from 'node:timers/promises'; + +import { defineCommand, runMain } from 'citty'; +import { consola } from 'consola'; +import { colors } from 'consola/utils'; +import type { TLPDB } from 'texlive-json-schemas/types'; +import { parseTemplate } from 'url-template'; + +namespace JSONL { + export function* parse( + text: string, + ): Generator { + for (let line of text.split(/\r?\n/gv)) { + line = line.trim(); + if (line.length > 0) { + yield JSON.parse(line) as unknown as T; + } + } + } +} + +namespace ctan { + export const MASTER_URL = new URL( + 'http://dante.ctan.org/tex-archive/systems/texlive/tlnet/', + ); + + export namespace api { + const url = parseTemplate('https://ctan.org/json/2.0/{/endpoint*}'); + + export interface Pkg { + id: string; + aliases?: { id: string }[]; + texlive?: string; + } + + export type Packages = { key: string }[]; + + export async function pkg(id: string): Promise { + return await getJson(url.expand({ endpoint: ['pkg', id] })); + } + + export async function packages(): Promise { + return await getJson(url.expand({ endpoint: ['packages'] })); + } + + async function getJson( + url: string | Readonly, + ): Promise { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`${res.url}: ${res.status} ${res.statusText}`); + } + return await res.json() as T; + } + } +} + +class Packages { + readonly #known = new Set(); + readonly #map = new Map>(); + + constructor(tlpdb: TLPDB) { + for (const { name, catalogue, cataloguedata } of tlpdb['main']!.tlpkgs!) { + if (catalogue !== undefined && catalogue !== null) { + this.addAlias(catalogue, name); + } + for (const alias of cataloguedata?.alias?.split(/\s+/gv) ?? []) { + if (alias.length > 0) { + this.addAlias(alias, name); + } + } + this.#known.add(name); + } + } + + addPkg(pkg: ctan.api.Pkg): void { + const name = pkg.texlive ?? null; + if (name !== null) { + this.addAlias(pkg.id, name); + for (const { id } of pkg.aliases ?? []) { + this.addAlias(id, name); + } + } else { + this.#known.add(pkg.id); + for (const { id } of pkg.aliases ?? []) { + this.#known.add(id); + } + } + } + + addAlias(alias: string, name: string): void { + this.#known.add(alias).add(name); + if (alias !== name) { + const aliases = this.#map.get(alias); + if (aliases instanceof Set) { + aliases.add(name); + } else { + this.#map.set( + alias, + aliases === undefined ? name : new Set([aliases, name]), + ); + } + } + } + + known(name: string): boolean { + return this.#known.has(name); + } + + getMap(): object { + return Object.fromEntries( + Array + .from( + this.#map.entries(), + ([key, value]) => + [key, (value instanceof Set) ? [...value].sort() : value] as const, + ) + .sort(([lhs], [rhs]) => lhs > rhs ? 1 : -1), + ); + } +} + +const encoding = 'utf8'; +const formatPath = (text: string) => colors.gray(colors.underline(text)); +const raw = (text: string) => text; + +async function loadOrGenerate( + jsonPath: string, + generate: () => Promise, +): Promise { + consola.info('Loading `%s`', path.basename(jsonPath)); + await using json = await open(jsonPath, 'a+'); + if ((await json.stat()).size !== 0) { + consola.ready('Found %s', formatPath(jsonPath)); + return JSON.parse(await json.readFile({ encoding })) as T; + } + consola.start('Generating %s', formatPath(jsonPath)); + const data = await generate(); + await json.writeFile(JSON.stringify(data, undefined, 2)! + '\n', encoding); + return data; +} + +async function dumpTlpdb(buildDir: string): Promise { + return await loadOrGenerate(path.join(buildDir, 'tlpdb.json'), async () => { + const args = [ + ['--repository', colors.magenta], + [ctan.MASTER_URL.toString(), colors.cyan], + ['dump-tlpdb', raw], + ['--remote', colors.magenta], + ['--json', colors.magenta], + ] as const; + consola.start( + 'Running %s %s _%s_ %s %s %s', + colors.yellow('tlmgr'), + ...args.map(([arg, fmt]) => fmt(arg)), + ); + const stdout = execFileSync('tlmgr', args.map(([arg]) => arg), { + stdio: ['ignore', 'pipe', 'inherit'], + encoding, + maxBuffer: 32 * 1024 * 1024, // 32mb + }); + return JSON.parse(stdout) as TLPDB; + }); +} + +async function getPackageList(buildDir: string): Promise { + return await loadOrGenerate( + path.join(buildDir, 'ctan-packages.json'), + ctan.api.packages, + ); +} + +interface Args { + build: string; + output: string; +} + +async function generate(args: Readonly): Promise { + const generated = new Date().toISOString(); + if (await mkdir(args.build, { recursive: true }) !== undefined) { + await writeFile(path.join(args.build, '.gitignore'), '*', encoding); + } + const packages = new Packages(await dumpTlpdb(args.build)); + + const jsonlPath = path.join(args.build, 'ctan-packages.jsonl'); + consola.info('Opening `%s`', path.basename(jsonlPath)); + await using jsonl = await open(jsonlPath, 'a+'); + for ( + const pkg of JSONL.parse(await jsonl.readFile({ encoding })) + ) { + packages.addPkg(pkg); + } + + const unknownPackages = (await getPackageList(args.build)) + .filter(({ key }) => !packages.known(key)) + .map(({ key }) => key) + .sort(); + let index = 0; + for (const id of unknownPackages) { + const progress = colors.gray(`[${++index}/${unknownPackages.length}]`); + consola.info('%s `%s`', progress, id); + const pkg = await ctan.api.pkg(id); + packages.addPkg(pkg); + jsonl.appendFile(JSON.stringify(pkg) + '\n', { flush: true, encoding }); + await setTimeout(1000); // 1s + } + + consola.info('Writing %s', formatPath(args.output)); + await mkdir(path.dirname(args.output), { recursive: true }); + const json = { toTL: packages.getMap(), generated }; + await writeFile( + args.output, + JSON.stringify(json, undefined, 2)! + '\n', + encoding, + ); + consola.log( + '\nDone %s package names recorded in %s', + colors.green(Object.keys(json.toTL).length), + colors.green(path.basename(args.output)), + ); +} + +const main = defineCommand({ + meta: { + name: path.basename(import.meta.filename), + description: 'Generate a package-name map from CTAN to TeX Live', + }, + args: { + build: { + type: 'string', + description: 'Build directory path', + default: 'build', + }, + output: { + alias: ['o'], + type: 'string', + description: 'Output file path', + required: true, + }, + }, + async run({ args }) { + await generate(args); + }, +}); + +runMain(main);