tldw
Version:
Generate README files from package metadata and configurable fragments.
116 lines (115 loc) • 18 kB
JavaScript
import e from"camelcase"
import t from"chalk"
import{filesize as n}from"filesize"
import*as a from"forward-slash-path"
import l from"fs-extra"
var r={legal:"Legal",warning:"Warning",usage:"Usage",advancedUsage:"Advanced Usage",example:!0,options:!0,development:!0,description:!0,result:!0,notes:"Notes",related:"Related",faq:"Questions & Answers"}
import o from"handlebars"
var i={flat:"secondary","flat-square":"secondary","for-the-badge":"default",plastic:"outline",social:"ghost"},s=new Set([".gif",".png",".svg"]),p=e=>e.replaceAll("-","--").replaceAll("_","__"),u=(e,t,n)=>{let a=e.replace(/\/+$/u,""),l=t.replace(/^\/+/,""),r=n.toString()
return r?`${a}/${l}?${r}`:`${a}/${l}`},g=e=>{let t=e.baseUrl??"https://shieldcn.dev",n=e.altText??"Shield",a=(e=>{try{let t=new URL(e).hostname
return"shieldcn.dev"===t||"www.shieldcn.dev"===t}catch{return!1}})(t),l=new URLSearchParams
if(a?l.set("variant",e.variant??i[e.style??"flat-square"]??e.style??"secondary"):l.set("style",e.style??"flat-square"),e.logo&&l.set("logo",e.logo),e.logoColor&&l.set("logoColor",e.logoColor),e.label&&l.set("label",e.label),e.query)for(let[t,n]of Object.entries(e.query))l.set(t,n)
let r=`badge/${encodeURIComponent(p(e.leftText??"Left"))}-${encodeURIComponent(p(e.rightText??"Right"))}-${encodeURIComponent(e.color??"lightgray")}`
e.path&&(r=Array.isArray(e.path)?e.path.map(e=>encodeURIComponent(e)).join("/"):(e=>e.split("/").map(e=>encodeURIComponent(e)).join("/"))(e.path)),e.path&&e.color&&l.set("color",e.color)
let o=a?(e=>(e=>[...s].some(t=>e.endsWith(t)))(e)?e:`${e}.svg`)(r):r,g=`<img src="${u(t,o,l)}" alt="${n}"/>`
if(e.colorSchemeAware&&a){let e=new URLSearchParams(l)
e.set("mode","light")
let a=new URLSearchParams(l)
a.set("mode","dark"),g=`<picture><source media="(prefers-color-scheme: dark)" srcset="${u(t,o,a)}"><img src="${u(t,o,e)}" alt="${n}"/></picture>`}return e.link?`<a href="${e.link}">${g}</a>`:g}
import*as m from"forward-slash-path"
import c from"fs-extra"
import f from"trim-around"
import{parse as d}from"yaml"
var h=new Intl.Collator(void 0),b=["bun","npm","pnpm","yarn","deno"],y=["npm"],k=["ts","tsx","js","jsx"],w=async e=>{if(!await c.pathExists(e))return null
if(!(await c.stat(e)).isFile())return null
let t=await Bun.file(e).text()
return f(t)},x=async e=>{let t=await w(e)
return null===t?null:d(t)??null},$=async e=>{for(let t of k){let n=await w(`${e}.${t}`)
if(null!==n)return{content:n,extension:t}}return null},v=async e=>(await $(e))?.content??null,j=e=>null!=e&&("string"==typeof e?e.trim().length>0:Array.isArray(e)?e.length>0:"object"!=typeof e||Object.keys(e).length>0),A=e=>{let t=[]
Array.isArray(e)?t=e:null!=e&&(t=[e])
let n=t.filter(e=>"string"==typeof e).map(e=>e.trim()).filter(Boolean)
return[...new Set(n)]},T=(e,t=y)=>{let n=null==e?t:A(e),a=[]
for(let e of n){let t=e.toLowerCase(),n=b.find(e=>e===t)
n&&!a.includes(n)&&a.push(n)}return a},C=(e,t=1)=>"number"==typeof e&&Number.isFinite(e)?Math.max(0,Math.floor(e)):t,L=e=>"string"==typeof e?e.trim()||null:(e=>"object"==typeof e&&null!==e&&!Array.isArray(e))(e)?e:null,S=e=>Array.isArray(e)?0===e.length?[]:e.some(Array.isArray)?e.map(e=>(Array.isArray(e)?e:[e]).map(L).filter(e=>null!==e)):[e.map(L).filter(e=>null!==e)]:null,B=e=>!1!==e&&null!=e&&(!0===e||("string"==typeof e?e.trim()||!0:"object"==typeof e&&!Array.isArray(e)&&e)),R=(e,t)=>{let n=t.trim().toLowerCase()
return A(e).some(e=>e.toLowerCase()===n)},F=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'"),E=e=>{if("string"==typeof e)return e
if(Array.isArray(e)){for(let t of e){let e=E(t)
if(e)return e}return null}return e?.url?e.url:null},I=e=>m.basename(e,m.extname(e)),U=e=>`https://raw.githubusercontent.com/${e}/HEAD/license.txt`,D=e=>e.split("/"),N=(e,t)=>{if(R(t.config.excludeShields,e))return""
if("bun"===e)return g({altText:"Bun",path:"badge/Bun-fbf0df",logo:"bun",logoColor:"fbf0df",variant:"outline",colorSchemeAware:!0,link:"https://bun.sh"})
if("types"===e)return g({altText:"TypeScript types included",path:"badge/types-included-377cc8",logo:"typescript",logoColor:"fff",colorSchemeAware:!0})
if("npm"===e)return g({altText:`${t.pkg.name} on npm`,leftText:"npm",rightText:t.pkg.name,logo:"npm",color:"C23039",link:`https://npmjs.com/package/${t.pkg.name}`})
if("npmx"===e)return g({altText:`${t.pkg.name} on npm`,leftText:"npmx",rightText:t.pkg.name,logo:"npm",color:"C23039",link:`https:/npmx.dev/package/${t.pkg.name}`})
if("pnpm"===e)return g({altText:`${t.pkg.name} on pnpm`,leftText:"pnpm",rightText:t.pkg.name,logo:"pnpm",logoColor:"white",color:"F69220",link:`https://npmjs.com/package/${t.pkg.name}`})
if("yarn"===e)return g({altText:`${t.pkg.name} on Yarn`,leftText:"Yarn",rightText:t.pkg.name,logo:"yarn",logoColor:"white",color:"2F8CB7",link:`https://yarnpkg.com/package/${t.pkg.name}`})
if("jsdelivr"===e)return g({altText:`${t.pkg.name} on jsDelivr`,leftText:"jsDelivr",rightText:t.pkg.name,logo:"html5",color:"orange",logoColor:"white",link:`https://www.jsdelivr.com/package/npm/${t.pkg.name}`})
if("unpkg"===e)return g({altText:`${t.pkg.name} on UNPKG`,leftText:"UNPKG",rightText:t.pkg.name,logo:"html5",color:"orange",logoColor:"white",link:`https://unpkg.com/browse/${t.pkg.name}/`})
if("web"===e)return g({altText:`${t.pkg.name} in the browser`,leftText:"browser",rightText:t.pkg.name,logo:"html5",color:"orange",logoColor:"white",link:`https://www.jsdelivr.com/package/npm/${t.pkg.name}`})
if("commitsSince"===e)return g({path:"badge/dynamic/json",query:{label:`commits since ${t.tag}`,query:"$.total_commits",url:`https://api.github.com/repos/${t.slug}/compare/${t.tag}...HEAD`},altText:`Commits since ${t.tag}`,logo:"github",link:`https://github.com/${t.slug}/compare/${t.tag}...HEAD`})
if("issues"===e)return g({path:["github","issues",...D(t.slug)],altText:"Issues",logo:"github",link:`https://github.com/${t.slug}/issues`})
if("license"===e)return g({path:["github","license",...D(t.slug)],altText:"License",link:U(t.slug)})
if("lastCommit"===e)return g({path:["github","last-commit",...D(t.slug)],altText:"Last commit",logo:"github",link:`https://github.com/${t.slug}/commits`})
if("githubPackages"===e){let e=`@${t.slug}`
return g({logo:"github",link:`https://github.com/${t.slug}/packages`,altText:`${e} on GitHub Packages`,leftText:"GitHub Packages",rightText:e,color:"24282e"})}if("dependents"===e)return g({path:["npm","dependents",...D(t.pkg.name)],altText:"Dependents",logo:"npm",link:`https://github.com/${t.slug}/network/dependents`})
if("npmDownloads"===e)return g({path:["npm","dm",...D(t.pkg.name)],altText:"Downloads",logo:"npm",link:`https://npmjs.com/package/${t.pkg.name}`})
if("npmLatest"===e)return g({path:["npm","v",...D(t.pkg.name)],altText:"Latest version on npm",logo:"npm",link:`https://npmjs.com/package/${t.pkg.name}`,label:"latest version"})
if("actions"===e)return t.config.githubActions?g({altText:"Build status",link:`https://github.com/${t.slug}/actions`,logo:"github",path:["github","ci",...D(t.slug)]}):""
if("sponsor"===e)return t.fundingLink?g({altText:`Sponsor ${t.pkg.name}`,link:t.fundingLink,leftText:"<3",rightText:"Sponsor",color:"FF45F1"}):""
throw Error(`Unknown shield type: ${e}`)},q=o.create()
q.registerHelper("shield",(e,...t)=>{let n=(e=>{let t=e.at(-1)
if("object"!=typeof t||null===t||!("data"in t))return null
let n=t.data
if("object"!=typeof n||null===n||!("root"in n))return null
let a=n.root
return"object"!=typeof a||null===a?null:a})(t)
return n?.config&&n.pkg&&n.slug&&n.tag?N(e,{config:n.config,fundingLink:n.fundingLink??null,pkg:n.pkg,slug:n.slug,tag:n.tag}):""}),q.registerHelper("escapeMarkdown",e=>"string"!=typeof e?"":e.replaceAll("<",String.raw`\<`))
var O,P=q,M=({bottomColor:e="oklch(66% 0.4 268)",font:t="JetBrains Mono, JetBrainsMono, monospace",text:n,topColor:a="oklch(70% 0.2 294)"})=>{let l=t,r=F(n)
return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16000 1000"><style>.text { font-size: 500px; font-weight: 200; font-family: ${l} }</style><defs><linearGradient id="color" x1="50%" y1="0%" x2="50%" y2="100%"><stop stop-color="${F(a)}"/><stop offset="100%" stop-color="${F(e)}"/></linearGradient></defs><rect width="16000" height="1000" fill="url(#color)" rx="100"/><text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="white" class="text">${r}</text></svg>`}
import*as H from"forward-slash-path"
import V from"fs-extra"
var G=async(e,t)=>{let n=await x(e)??{},a=await V.pathExists(H.join(t,".github","workflows")),l=n.environmentVariables,r={}
return"object"!=typeof l||null===l||Array.isArray(l)||(r=l),{banner:!1,binExample:null,binName:"global"===n.installation,githubActions:a,personal:!1,link:null,linkName:null,installation:!1,environmentVariables:{},maxBlankLines:1,needsNodeRuntime:!0,packageManagers:[...y],tryInBrowser:null,versionInInstallation:!1,exampleResultMayVary:!1,excludeShields:[],renderComment:!0,shields:null,githubPackage:!1,...n,banner:B(n.banner),environmentVariables:r,excludeShields:A(n.excludeShields),maxBlankLines:C(n.maxBlankLines),packageManagers:T(n.packageManagers,y),shields:S(n.shields),versionInInstallation:!0===n.versionInInstallation}}
import*as W from"forward-slash-path"
import{globby as J}from"globby"
var _,z=async({configDirectory:e})=>{let t=await J(k.map(e=>`result*.${e}`),{cwd:e,onlyFiles:!0,caseSensitiveMatch:!0}),n=[...new Set(t.map(I).filter(e=>"result"!==e))].toSorted((e,t)=>h.compare(e,t)),a=await Promise.all(n.map(async t=>{for(let n of k){let a=W.join(e,`${t}.${n}`),l=await w(a)
if(null!==l)return[t,l]}return null}))
return Object.fromEntries(a.filter(e=>null!==e))}
import*as K from"forward-slash-path"
var Y=async e=>{let t=await(async e=>{let t=await w(e)
return null===t?null:JSON.parse(t)})(e)??{},n="string"==typeof t.name&&t.name.length>0?t.name:K.basename(K.dirname(K.resolve(e))),a="string"==typeof t.version&&t.version.length>0?t.version:"1.0.0"
return{...t,name:n,version:a}}
import*as Q from"forward-slash-path"
var X=(e,t)=>e.required&&!t.required?-1:t.required&&!e.required?1:h.compare(e.name,t.name),Z=async(e,t)=>{let[n,a]=await Promise.all([x(e),x(Q.join(t,"action.yml"))]),l=Object.entries(n??{}).map(([e,t])=>((e,t)=>"object"!=typeof t||null===t||Array.isArray(t)?{name:e,default:t}:{name:e,...t})(e,t))
if(a?.inputs)for(let[e,t]of Object.entries(a.inputs)){let n=l.find(t=>t.name===e)
n||(n={name:e},l.push(n)),void 0===n.info&&"string"==typeof t.description&&(n.info=t.description),void 0===n.default&&Object.hasOwn(t,"default")&&(n.default=t.default),void 0===n.required&&"boolean"==typeof t.required&&(n.required=t.required)}return 0===l.length?null:(l.sort(X),{entries:l,anyEntryHasType:l.some(e=>j(e.type)),anyEntryHasInfo:l.some(e=>j(e.info)),anyEntryHasRequired:l.some(e=>!0===e.required),anyEntryHasDefault:l.some(e=>j(e.default))})},ee=e=>"dev"===e?"--save-dev ":"global"===e?"--global ":"--save ",te=(e,t,n)=>n?`${e}@^${t}`:e,ne={bun:{prod:e=>`bun add ${e}`,dev:e=>`bun add --development ${e}`,global:e=>`bun add --global ${e}`},npm:{prod:e=>`npm install --save ${e}`,dev:e=>`npm install --save-dev ${e}`,global:e=>`npm install --global ${e}`},pnpm:{prod:e=>`pnpm add ${e}`,dev:e=>`pnpm add --save-dev ${e}`,global:e=>`pnpm add --global ${e}`},yarn:{prod:e=>`yarn add ${e}`,dev:e=>`yarn add --dev ${e}`,global:e=>`yarn global add ${e}`},deno:{prod:e=>`deno add npm:${e}`,dev:e=>`deno add --dev npm:${e}`,global:e=>`deno install --global npm:${e}`}},ae=["bun.lock","bun.toml","bunfig.toml"],le=async e=>(await Promise.all(ae.map(t=>l.pathExists(a.join(e,t))))).some(Boolean),re=async t=>{if(!await l.pathExists(t.packageFile))throw Error(`No package.json found at ${t.packageFile}.`)
let n=a.dirname(t.packageFile),o=Object.entries(r).map(async([e,n])=>{let l=a.join(t.configDirectory,`${e}.md`),r=await w(l)
return null===r?[e,null]:"usage"===e?[e,r]:"string"==typeof n?[e,`## ${n}\n\n${r}`]:[e,r]}),[i,s,p,u,m,c,f,d,b,y,k,...A]=await Promise.all([Y(t.packageFile),G(a.join(t.configDirectory,"config.yml"),n),Z(a.join(t.configDirectory,"usageOptions.yml"),n),v(a.join(t.configDirectory,"example")),$(a.join(t.configDirectory,"usage")),$(a.join(t.configDirectory,"result")),z(t),w(t.licenseFile),x(a.join(t.configDirectory,"envVars.yml")),(_??=(async()=>{let e=await Bun.file(new URL("../../package.json",import.meta.url)).json()
return{name:e.name??"tldw",description:e.description??"Generate README files from package metadata and configurable fragments.",version:e.version??"0.0.0"}})(),_),le(n),...o]),T=(e=>"string"==typeof e?e:e?.url?e.url:null)(i.repository),C=T?(e=>{let t=e.trim().replace(/^git\+/u,"").replace(/[#?].*$/u,"").replace(/\.git$/u,"").replace(/\/$/u,""),n=[/^github:(?<owner>[^/]+)\/(?<repo>[^/]+)$/u,/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^/]+)$/iu,/^(?<owner>[^/]+)\/(?<repo>[^/]+)$/u]
for(let e of n){let n=e.exec(t)
if(n?.groups?.owner&&n.groups.repo)return`${n.groups.owner}/${n.groups.repo}`}return null})(T):null
if(!C)return null
let L={...s.environmentVariables,...b}
null===s.link&&i.domain&&(s.link=`https://${i.domain}`),s.link&&null===s.linkName&&(s.linkName=(e=>{try{return new URL(e).host}catch{return null}})(s.link))
let S=!s.needsNodeRuntime
null===s.tryInBrowser&&(s.tryInBrowser=S)
let B=!1
!0===s.binName?B=(e=>{if(e.bin&&"object"==typeof e.bin&&!Array.isArray(e.bin)){let[t]=Object.keys(e.bin)
if(t)return t}if(e.name.startsWith("@")){let[,t=e.name]=e.name.split("/")
return t}return e.name})(i):"string"==typeof s.binName&&(B=s.binName)
let F=s.binExample||B||null,I=A.filter(e=>null!==e[1]),U=Object.fromEntries(I),D=U.usage??null,q=m?.content??null,O=c?.content??null,P=d,H=!1
if(P?.startsWith("MIT License")){let e=P.split(/\r?\n/u).find(e=>e.startsWith("Copyright"))
e&&(P=e),H=!0}let V=j(L)?(e=>Object.fromEntries(Object.entries(e).toSorted(([e],[t])=>h.compare(e,t))))(L):{},W=(e=>{let t=[],n=te(e.pkg.name,e.pkg.version,e.config.versionInInstallation)
if(e.config.installation)for(let a of(e=>null==e?[]:Array.isArray(e)?[...e]:[e])(e.config.packageManagers))t.push({header:a,headerArgument:e.pkg.name,command:ne[a][e.config.installation](n)})
return e.config.githubPackage&&e.config.installation&&t.push({header:"githubPackages",headerArgument:e.slug,bonusText:"(if [configured properly](https://help.github.com/en/github/managing-packages-with-github-packages/configuring-npm-for-use-with-github-packages))",command:`npm install ${ee(e.config.installation)}${te(`@${e.slug}`,e.pkg.version,e.config.versionInInstallation)}`}),t})({config:s,pkg:i,slug:C}),J=((e,t)=>{let n=[{name:"setting up",script:`git clone git@github.com:${t}.git\ncd ${e.name}\nbun install`}]
return e.scripts?.lint&&n.push({name:"linting",script:"bun run lint"}),e.scripts?.typecheck&&n.push({name:"type checking",script:"bun run typecheck"}),e.scripts?.test&&n.push({name:"testing",script:"bun run test"}),e.scripts?.["test:dev"]&&n.push({name:"testing in development environment",script:"bun run test:dev"}),n})(i,C),K=null!==p,Q=i.displayName||i.title||i.domain||i.name,X=(e=>!1===e.config.banner?null:!0===e.config.banner?M({text:e.title}):"string"==typeof e.config.banner?M({text:e.config.banner}):M({bottomColor:e.config.banner.bottomColor,font:e.config.banner.font,text:e.config.banner.text||e.title,topColor:e.config.banner.topColor}))({config:s,title:Q}),ae=(e=>(e.config.shields??(e=>[["npmLatest",...j(e.pkg.license)?["license"]:[],...e.isBunProject?["bun"]:[]]])(e)).map(t=>(Array.isArray(t)?t:[t]).map(t=>((e,t)=>"string"==typeof e?N(e,t):e.id&&R(t.config.excludeShields,e.id)?"":g(e))(t,e)).filter(Boolean).join(" ").trim()).filter(Boolean))({config:s,fundingLink:E(i.funding),installationCommands:W,isBunProject:k,pkg:i,slug:C,tag:`v${i.version}`}),re=E(i.funding)
return{args:t,bannerSvg:X,binExample:F,binName:B,camelCaseName:e(i.name),config:{...s,environmentVariables:V},description:i.description??null,developmentScripts:J,example:u,exampleResults:f,fragments:U,fundingLink:re,globalName:i.webpackConfigJaid?.endsWith("Class")?e(i.name,{pascalCase:!0}):e(i.name),hasDevelopmentSection:Boolean(U.development)||J.length>0,hasEnvironmentVariables:j(V),hasExampleSection:Boolean(u||Object.keys(f).length>0||U.example),hasOptionsSection:K||Boolean(U.options),hasUsageOptions:K,hasUsageSection:Boolean(D||q||O),installationCommands:W,isBunProject:k,isMitLicense:H,license:P,pascalCaseName:e(i.name,{pascalCase:!0}),pkg:i,shieldLines:ae,slug:C,tag:`v${i.version}`,title:Q,tldwVersion:y.version,usage:D,usageCode:q,usageCodeLanguage:m?.extension??"ts",usageOptions:p,usageResult:O,usageResultLanguage:c?.extension??"ts",worksAsScriptTag:S}},oe=async e=>{let t=await re(e)
if(!t)return{status:"skipped",outputFile:e.outputFile,reason:"tldw is made for GitHub repositories, but package.json#repository is not set. Doing nothing."}
let n=((e,t)=>{let n=e.replaceAll(/\r\n?/gu,"\n").split("\n"),a=[],l=0,r=null
for(let e of n){let n=/^(?<indent>\s*)(?<marker>`{3,}|~{3,})/u.exec(e)?.groups?.marker??null
n&&(r===n?r=null:null===r&&(r=n))
let o=0===e.trim().length
if(null===r&&o){if(l++,l>t)continue}else l=0
a.push(e)}return`${a.join("\n").trim()}\n`})((e=>`${e.replaceAll(/\r\n?/gu,"\n").trim()}\n`)(await(async e=>(await(O??=(async()=>{let e=await Bun.file(new URL("template.hbs",import.meta.url)).text()
return P.compile(e)})(),O))(e))(t)),t.config.maxBlankLines),a=await l.pathExists(e.outputFile)?await Bun.file(e.outputFile).text():null,r=Buffer.byteLength(n)
return a===n?{status:"unchanged",outputFile:e.outputFile,readmeText:n,bytes:r}:(await l.outputFile(e.outputFile,n),{status:null===a?"created":"overwritten",outputFile:e.outputFile,readmeText:n,bytes:r})},ie=(e,l=process.cwd())=>{if("skipped"===e.status)return void(e.reason&&console.warn(e.reason))
let r=a.relative(l,e.outputFile)||a.basename(e.outputFile),o="Left unchanged"
"created"===e.status?o="Created":"overwritten"===e.status&&(o="Overwrote"),console.log(`${t.green(o)} ${t.yellow(r)} ${t.green("with")} ${t.yellow(n(e.bytes??0))}`)},se=oe
export{oe as writeReadme,ie as logWriteReadmeResult,se as default,re as createReadmeContext}