build(types): provide type hints via JSDoc

This commit is contained in:
Rafael Bardini 2023-09-28 02:50:43 +02:00
parent 6ecc806add
commit dec4e36093
29 changed files with 1270 additions and 18 deletions

View File

@ -17,6 +17,9 @@ jobs:
- name: Install
run: npm ci
- name: Type-check
run: npm run type-check
- name: Lint
run: npm run lint

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ node_modules
public
npm-debug.log
resume.html
schema.d.ts

View File

@ -15,6 +15,7 @@ Inspired by [jsonresume-theme-flat](https://github.com/erming/jsonresume-theme-f
- 🎨 Customizable colors
- 🧩 Standalone CLI
- 📦 ESM and CommonJS builds
- 🤖 TypeScript typings
[View demo →](https://jsonresume-theme-even.rbrd.in)

View File

@ -2,6 +2,10 @@ import html from '../utils/html.js'
import markdown from '../utils/markdown.js'
import Date from './date.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['awards']} awards
* @returns {string | false}
*/
export default function Awards(awards = []) {
return awards.length > 0 && html`
<section id="awards">

View File

@ -2,6 +2,10 @@ import html from '../utils/html.js'
import Date from './date.js'
import Link from './link.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['certificates']} certificates
* @returns {string | false}
*/
export default function Certificates(certificates = []) {
return certificates.length > 0 && html`
<section id="certificates">

View File

@ -1,5 +1,9 @@
import html from '../utils/html.js'
/**
* @param {string} dateString
* @returns {string}
*/
const formatDate = dateString =>
new Date(dateString).toLocaleDateString('en', {
month: 'short',
@ -7,6 +11,10 @@ const formatDate = dateString =>
timeZone: 'UTC',
})
/**
* @param {string} date
* @returns {string}
*/
export default function Duration(date) {
return html`<time datetime="${date}">${formatDate(date)}</time>`
}

View File

@ -1,6 +1,11 @@
import html from '../utils/html.js'
import Date from './date.js'
/**
* @param {string} startDate
* @param {string} [endDate]
* @returns {string}
*/
export default function Duration(startDate, endDate) {
return html`${Date(startDate)} ${endDate ? Date(endDate) : 'Present'}`
}

View File

@ -3,6 +3,10 @@ import markdown from '../utils/markdown.js'
import Duration from './duration.js'
import Link from './link.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['education']} education
* @returns {string | false}
*/
export default function Education(education = []) {
return education.length > 0 && html`
<section id="education">
@ -14,7 +18,7 @@ export default function Education(education = []) {
<h4>${Link(url, institution)}</h4>
<div class="meta">
${area && html`<strong>${area}</strong>`}
<div>${Duration(startDate, endDate)}</div>
${startDate && html`<div>${Duration(startDate, endDate)}</div>`}
</div>
</header>
${studyType && markdown(studyType)}

View File

@ -3,10 +3,18 @@ import markdown from '../utils/markdown.js'
import Icon from './icon.js'
import Link from './link.js'
/**
* @param {string} countryCode
* @returns {string | undefined}
*/
const formatCountry = countryCode => Intl.DisplayNames
? new Intl.DisplayNames(['en'], { type: 'region' }).of(countryCode)
: countryCode
/**
* @param {import('../schema.d.ts').ResumeSchema['basics']} basics
* @returns {string}
*/
export default function Header(basics = {}) {
const { email, image, label, location, name, phone, profiles = [], summary, url } = basics
@ -45,7 +53,7 @@ export default function Header(basics = {}) {
`}
${profiles.map(({ network, url, username }) => html`
<li>
${Icon(network, 'user')}
${network && Icon(network, 'user')}
${Link(url, username)}
${network && html`<span class="network">(${network})</span>`}
</li>

View File

@ -1,7 +1,15 @@
import feather from 'feather-icons'
/** @typedef {import('feather-icons').FeatherIconNames} FeatherIconNames */
/**
* @param {string} name
* @param {string} [fallback]
* @returns {string | undefined}
*/
export default function Icon(name, fallback) {
const icon =
feather.icons[name.toLowerCase()] || feather.icons[fallback.toLowerCase()]
return icon.toSvg({ width: 16, height: 16 })
feather.icons[/** @type {FeatherIconNames} */ (name.toLowerCase())] ||
(fallback && feather.icons[/** @type {FeatherIconNames} */ (fallback.toLowerCase())])
return icon?.toSvg({ width: 16, height: 16 })
}

View File

@ -1,5 +1,9 @@
import html from '../utils/html.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['interests']} interests
* @returns {string | false}
*/
export default function Interests(interests = []) {
return interests.length > 0 && html`
<section id="interests">

View File

@ -1,5 +1,9 @@
import html from '../utils/html.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['languages']} languages
* @returns {string | false}
*/
export default function Languages(languages = []) {
return languages.length > 0 && html`
<section id="languages">

View File

@ -1,7 +1,16 @@
import html from '../utils/html.js'
/**
* @param {string} url
* @returns {string}
*/
const formatURL = url => url.replace(/^(https?:|)\/\//, '').replace(/\/$/, '')
/**
* @param {string} [url]
* @param {string} [name]
* @returns {string | undefined}
*/
export default function Link(url, name) {
return name
? (url ? html`<a href="${url}">${name}</a>` : name)

View File

@ -1,6 +1,10 @@
import html from '../utils/html.js'
import markdown from '../utils/markdown.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['basics']} basics
* @returns {string}
*/
export default function Meta(basics = {}) {
const { name, summary } = basics

View File

@ -3,10 +3,18 @@ import markdown from '../utils/markdown.js'
import Duration from './duration.js'
import Link from './link.js'
const formatRoles = arr => Intl.ListFormat
? new Intl.ListFormat('en').format(arr)
: arr.join(', ')
/**
* @param {string[]} roles
* @returns {string}
*/
const formatRoles = roles => Intl.ListFormat
? new Intl.ListFormat('en').format(roles)
: roles.join(', ')
/**
* @param {import('../schema.d.ts').ResumeSchema['projects']} projects
* @returns {string | false}
*/
export default function Projects(projects = []) {
return projects.length > 0 && html`
<section id="projects">
@ -21,7 +29,7 @@ export default function Projects(projects = []) {
${roles.length > 0 && html`<strong>${formatRoles(roles)}</strong>`}
${entity && html`at <strong>${entity}</strong>`}
</div>
<div>${Duration(startDate, endDate)}</div>
${startDate && html`<div>${Duration(startDate, endDate)}</div>`}
${type && html`<div>${type}</div>`}
</div>
</header>

View File

@ -3,6 +3,10 @@ import markdown from '../utils/markdown.js'
import Date from './date.js'
import Link from './link.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['publications']} publications
* @returns {string | false}
*/
export default function Publications(publications = []) {
return publications.length > 0 && html`
<section id="publications">

View File

@ -1,6 +1,10 @@
import html from '../utils/html.js'
import markdown from '../utils/markdown.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['references']} references
* @returns {string | false}
*/
export default function References(references = []) {
return references.length > 0 && html`
<section id="references">

View File

@ -1,5 +1,9 @@
import html from '../utils/html.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['skills']} skills
* @returns {string | false}
*/
export default function Skills(skills = []) {
return skills.length > 0 && html`
<section id="skills">

View File

@ -3,6 +3,10 @@ import markdown from '../utils/markdown.js'
import Duration from './duration.js'
import Link from './link.js'
/**
* @param {import('../schema.d.ts').ResumeSchema['volunteer']} volunteer
* @returns {string | false}
*/
export default function Volunteer(volunteer = []) {
return volunteer.length > 0 && html`
<section id="volunteer">
@ -14,7 +18,7 @@ export default function Volunteer(volunteer = []) {
<h4>${Link(url, organization)}</h4>
<div class="meta">
<strong>${position}</strong>
<div>${Duration(startDate, endDate)}</div>
${startDate && html`<div>${Duration(startDate, endDate)}</div>`}
</div>
</header>
${summary && markdown(summary)}

View File

@ -3,13 +3,21 @@ import markdown from '../utils/markdown.js'
import Duration from './duration.js'
import Link from './link.js'
/** @typedef {NonNullable<import('../schema.d.ts').ResumeSchema['work']>[number]} Work */
/** @typedef {Pick<Work, 'highlights' | 'location' | 'position' | 'startDate' | 'endDate' | 'summary'>} NestedWorkItem */
/** @typedef {Pick<Work, 'description' | 'name' | 'url'> & { items: NestedWorkItem[] }} NestedWork */
/**
* @param {import('../schema.d.ts').ResumeSchema['work']} work
* @returns {string | false}
*/
export default function Work(work = []) {
const nestedWork = work.reduce((acc, { description, name, url, ...rest }) => {
const prev = acc[acc.length - 1]
if (prev && prev.name === name && prev.description === description && prev.url === url) prev.items.push(rest)
else acc.push({ description, name, url, items: [rest] })
return acc
}, [])
}, /** @type {NestedWork[]} */ ([]))
return work.length > 0 && html`
<section id="work">
@ -29,7 +37,7 @@ export default function Work(work = []) {
<div>
<h5>${position}</h5>
<div class="meta">
<div>${Duration(startDate, endDate)}</div>
${startDate && html`<div>${Duration(startDate, endDate)}</div>`}
${location && html`<div>${location}</div>`}
</div>
</div>

View File

@ -1,8 +1,13 @@
import Resume from './resume.js'
// @ts-expect-error `?inline` query
import css from './style.css?inline'
export const pdfRenderOptions = { mediaType: 'print' }
/**
* @param {import('./schema.d.ts').ResumeSchema} resume
* @returns {string}
*/
export const render = resume => {
return Resume(resume, css)
}

1095
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@
"main": "./dist/index.cjs",
"unpkg": "./dist/index.umd.cjs",
"module": "./dist/index.js",
"typings": "./dist/index.d.ts",
"source": "index.js",
"bin": {
"jsonresume-theme-even": "bin/cli.js"
@ -41,9 +42,10 @@
"dev": "vite",
"format": "prettier .",
"lint": "eslint --ignore-path .gitignore .",
"prepare": "husky install",
"prepare": "husky install && json2ts node_modules/resume-schema/schema.json schema.d.ts",
"prepublishOnly": "npm run build",
"test": "vitest"
"test": "vitest",
"type-check": "tsc"
},
"dependencies": {
"feather-icons": "^4.28.0",
@ -55,17 +57,22 @@
"@codemirror/lang-json": "6.0.1",
"@codemirror/theme-one-dark": "6.1.2",
"@codemirror/view": "6.19.0",
"@types/feather-icons": "4.29.2",
"@vitest/coverage-v8": "0.34.3",
"codemirror": "6.0.1",
"debounce": "1.2.1",
"eslint": "8.48.0",
"html-validate": "8.3.0",
"husky": "8.0.3",
"json-schema-to-typescript": "13.1.1",
"lint-staged": "14.0.1",
"prettier": "3.0.2",
"prettier-plugin-packagejson": "2.4.5",
"resume-schema": "1.0.0",
"typescript": "5.2.2",
"vite": "4.4.9",
"vite-plugin-dts": "3.5.3",
"vite-plugin-static-copy": "0.17.0",
"vitest": "0.34.3"
}
}

View File

@ -14,6 +14,11 @@ import Work from './components/work.js'
import colors from './utils/colors.js'
import html from './utils/html.js'
/**
* @param {import('./schema.d.ts').ResumeSchema} resume
* @param {string} css
* @returns
*/
export default function Resume(resume, css) {
return html`<!DOCTYPE html>
<html lang="en" style="${colors(resume.meta)}">

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"include": ["./index.js"],
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"noEmit": true,
"strict": true,
"target": "esnext"
}
}

View File

@ -1,5 +1,12 @@
/** @typedef {Record<string, [light: string, dark?: string]>} ThemeColorOptions */
/** @typedef {{ colors?: ThemeColorOptions }} ThemeOptions */
/**
* @param {import('../schema.d.ts').ResumeSchema['meta'] & { themeOptions?: ThemeOptions }} meta
* @returns {string | undefined}
*/
export default function colors(meta = {}) {
const { colors } = meta.themeOptions || {}
const colors = meta.themeOptions?.colors
return (
colors &&
Object.entries(colors)

View File

@ -1,4 +1,9 @@
// Based on https://github.com/jimniels/html
/**
* @param {TemplateStringsArray} strings
* @param {...any} values
* @returns {string}
*/
export default function html(strings, ...values) {
return strings.reduce((acc, string, i) => {
const value = values[i]

View File

@ -1,7 +1,13 @@
import micromark from 'micromark'
import striptags from 'striptags'
/**
* @param {string} doc
* @param {boolean} [stripTags]
* @returns
*/
export default function markdown(doc, stripTags = false) {
const html = micromark(doc)
// @ts-expect-error missing micromark types
const html = /** @type {string} */ (micromark(doc))
return stripTags ? striptags(html) : html
}

View File

@ -1,4 +1,6 @@
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import pkg from './package.json' assert { type: 'json' }
export default defineConfig(({ mode }) => {
@ -25,9 +27,15 @@ export default defineConfig(({ mode }) => {
external: [...Object.keys(pkg.dependencies), /^node:.*/],
},
target: 'esnext',
test: {
clearMocks: true,
},
},
plugins: [
dts(),
viteStaticCopy({
targets: [{ src: './schema.d.ts', dest: '.' }],
}),
],
test: {
clearMocks: true,
},
}
})