From 85a44878e0670d2e098bcfb537e71893e86005a2 Mon Sep 17 00:00:00 2001 From: Jack Platten Date: Mon, 25 May 2026 08:30:38 -0700 Subject: [PATCH] Format with biome --- .changeset/config.json | 18 ++-- biome.json | 34 +++++++ package.json | 109 +++++++++++----------- pnpm-lock.yaml | 95 +++++++++++++++++++ src/commands/init.ts | 188 +++++++++++++++++++------------------- src/commands/new.ts | 128 +++++++++++++------------- src/commands/publish.ts | 94 +++++++++---------- src/commands/unpublish.ts | 78 ++++++++-------- src/commands/update.ts | 78 ++++++++-------- src/index.ts | 38 ++++---- src/lib/commands.ts | 67 ++++++++------ src/lib/deps.ts | 76 +++++++-------- src/lib/tags.ts | 117 +++++++++++++----------- tsconfig.json | 44 ++++----- tsdown.config.ts | 14 +-- 15 files changed, 665 insertions(+), 513 deletions(-) create mode 100644 biome.json diff --git a/.changeset/config.json b/.changeset/config.json index 5c58ec9..d66dee8 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,11 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "fixed": [], - "linked": [], - "access": "restricted", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [] + "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..5076b58 --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**", "!!**/dist"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package.json b/package.json index 104506f..8d2a192 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,57 @@ { - "name": "@plttn/mkd", - "version": "0.2.2", - "description": "A tool for managing markdown blog posts", - "scripts": { - "dev": "tsx ./src/index.ts", - "build": "tsdown", - "prepublishOnly": "pnpm run build", - "ci:publish": "pnpm build && changeset publish" - }, - "type": "module", - "bin": { - "mkd": "dist/index.cjs" - }, - "files": [ - "dist" - ], - "main": "./dist/index.cjs", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, - "./package.json": "./package.json" - }, - "keywords": [], - "author": "", - "license": "MIT", - "packageManager": "pnpm@11.3.0", - "devDependencies": { - "@changesets/cli": "^2.31.0", - "@types/node": "^25.9.1", - "prettier": "3.8.3", - "tsdown": "^0.22.0", - "tsx": "^4.22.3", - "typescript": "^6.0.3" - }, - "dependencies": { - "@clack/prompts": "^1.4.0", - "@sindresorhus/slugify": "^3.0.0", - "cmd-ts": "^0.15.0", - "filenamify": "^7.0.1", - "gray-matter": "^4.0.3", - "pwd-fs": "^3.5.4" - }, - "repository": { - "type": "git", - "url": "https://github.com/plttn/mkd.git" - }, - "publishConfig": { - "access": "public", - "provenance": true - } + "name": "@plttn/mkd", + "version": "0.2.2", + "description": "A tool for managing markdown blog posts", + "scripts": { + "dev": "tsx ./src/index.ts", + "build": "tsdown", + "prepublishOnly": "pnpm run build", + "ci:publish": "pnpm build && changeset publish" + }, + "type": "module", + "bin": { + "mkd": "dist/index.cjs" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "keywords": [], + "author": "", + "license": "MIT", + "packageManager": "pnpm@11.3.0", + "devDependencies": { + "@biomejs/biome": "2.4.15", + "@changesets/cli": "^2.31.0", + "@types/node": "^25.9.1", + "prettier": "3.8.3", + "tsdown": "^0.22.0", + "tsx": "^4.22.3", + "typescript": "^6.0.3" + }, + "dependencies": { + "@clack/prompts": "^1.4.0", + "@sindresorhus/slugify": "^3.0.0", + "cmd-ts": "^0.15.0", + "filenamify": "^7.0.1", + "gray-matter": "^4.0.3", + "pwd-fs": "^3.5.4" + }, + "repository": { + "type": "git", + "url": "https://github.com/plttn/mkd.git" + }, + "publishConfig": { + "access": "public", + "provenance": true + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de13622..d520799 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -280,6 +280,9 @@ importers: specifier: ^3.5.4 version: 3.5.4 devDependencies: + '@biomejs/biome': + specifier: 2.4.15 + version: 2.4.15 '@changesets/cli': specifier: ^2.31.0 version: 2.31.0(@types/node@25.9.1) @@ -331,6 +334,63 @@ packages: resolution: {integrity: sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==} engines: {node: ^22.18.0 || >=24.11.0} + '@biomejs/biome@2.4.15': + resolution: {integrity: sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.15': + resolution: {integrity: sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.15': + resolution: {integrity: sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.15': + resolution: {integrity: sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.15': + resolution: {integrity: sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.15': + resolution: {integrity: sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.15': + resolution: {integrity: sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.15': + resolution: {integrity: sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.15': + resolution: {integrity: sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -1305,6 +1365,41 @@ snapshots: '@babel/helper-string-parser': 8.0.0-rc.5 '@babel/helper-validator-identifier': 8.0.0-rc.5 + '@biomejs/biome@2.4.15': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.15 + '@biomejs/cli-darwin-x64': 2.4.15 + '@biomejs/cli-linux-arm64': 2.4.15 + '@biomejs/cli-linux-arm64-musl': 2.4.15 + '@biomejs/cli-linux-x64': 2.4.15 + '@biomejs/cli-linux-x64-musl': 2.4.15 + '@biomejs/cli-win32-arm64': 2.4.15 + '@biomejs/cli-win32-x64': 2.4.15 + + '@biomejs/cli-darwin-arm64@2.4.15': + optional: true + + '@biomejs/cli-darwin-x64@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-arm64@2.4.15': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.15': + optional: true + + '@biomejs/cli-linux-x64@2.4.15': + optional: true + + '@biomejs/cli-win32-arm64@2.4.15': + optional: true + + '@biomejs/cli-win32-x64@2.4.15': + optional: true + '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 diff --git a/src/commands/init.ts b/src/commands/init.ts index 53beb04..648e236 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,111 +1,111 @@ +import { intro, isCancel, outro, text } from "@clack/prompts"; import { command } from "cmd-ts"; -import { text, isCancel, intro, outro } from "@clack/prompts"; import type { Deps } from "../lib/deps"; export function makeInitCommand({ config, pfs }: Deps) { - return command({ - name: "init", - description: "Create or update mkd.json configuration", - args: {}, - handler: async () => { - intro("Initialize mkd configuration"); + return command({ + name: "init", + description: "Create or update mkd.json configuration", + args: {}, + handler: async () => { + intro("Initialize mkd configuration"); - const blogDir = await text({ - message: "Directory where generated posts are written", - defaultValue: String(config.blogDir ?? "./src/blog"), - }); - if (isCancel(blogDir)) { - outro("Init cancelled"); - return; - } + const blogDir = await text({ + message: "Directory where generated posts are written", + defaultValue: String(config.blogDir ?? "./src/blog"), + }); + if (isCancel(blogDir)) { + outro("Init cancelled"); + return; + } - const author = await text({ - message: "Default author", - defaultValue: String(config.author ?? ""), - }); - if (isCancel(author)) { - outro("Init cancelled"); - return; - } + const author = await text({ + message: "Default author", + defaultValue: String(config.author ?? ""), + }); + if (isCancel(author)) { + outro("Init cancelled"); + return; + } - const titleKey = await text({ - message: "Title frontmatter key", - defaultValue: String(config.titleKey ?? "title"), - }); - if (isCancel(titleKey)) { - outro("Init cancelled"); - return; - } + const titleKey = await text({ + message: "Title frontmatter key", + defaultValue: String(config.titleKey ?? "title"), + }); + if (isCancel(titleKey)) { + outro("Init cancelled"); + return; + } - const publishedAtKey = await text({ - message: "Published date frontmatter key", - defaultValue: String(config.publishedAtKey ?? "publishedAt"), - }); - if (isCancel(publishedAtKey)) { - outro("Init cancelled"); - return; - } + const publishedAtKey = await text({ + message: "Published date frontmatter key", + defaultValue: String(config.publishedAtKey ?? "publishedAt"), + }); + if (isCancel(publishedAtKey)) { + outro("Init cancelled"); + return; + } - const modifiedAtKey = await text({ - message: "Modified date frontmatter key", - defaultValue: String(config.modifiedAtKey ?? "updatedAt"), - }); - if (isCancel(modifiedAtKey)) { - outro("Init cancelled"); - return; - } + const modifiedAtKey = await text({ + message: "Modified date frontmatter key", + defaultValue: String(config.modifiedAtKey ?? "updatedAt"), + }); + if (isCancel(modifiedAtKey)) { + outro("Init cancelled"); + return; + } - const authorKey = await text({ - message: "Author frontmatter key", - defaultValue: String(config.authorKey ?? "author"), - }); - if (isCancel(authorKey)) { - outro("Init cancelled"); - return; - } + const authorKey = await text({ + message: "Author frontmatter key", + defaultValue: String(config.authorKey ?? "author"), + }); + if (isCancel(authorKey)) { + outro("Init cancelled"); + return; + } - const draftKey = await text({ - message: "Draft frontmatter key", - defaultValue: String(config.draftKey ?? "draft"), - }); - if (isCancel(draftKey)) { - outro("Init cancelled"); - return; - } + const draftKey = await text({ + message: "Draft frontmatter key", + defaultValue: String(config.draftKey ?? "draft"), + }); + if (isCancel(draftKey)) { + outro("Init cancelled"); + return; + } - const descriptionKey = await text({ - message: "Description frontmatter key", - defaultValue: String(config.descriptionKey ?? "description"), - }); - if (isCancel(descriptionKey)) { - outro("Init cancelled"); - return; - } + const descriptionKey = await text({ + message: "Description frontmatter key", + defaultValue: String(config.descriptionKey ?? "description"), + }); + if (isCancel(descriptionKey)) { + outro("Init cancelled"); + return; + } - const tagsKey = await text({ - message: "Tags frontmatter key", - defaultValue: String(config.tagsKey ?? "tags"), - }); - if (isCancel(tagsKey)) { - outro("Init cancelled"); - return; - } + const tagsKey = await text({ + message: "Tags frontmatter key", + defaultValue: String(config.tagsKey ?? "tags"), + }); + if (isCancel(tagsKey)) { + outro("Init cancelled"); + return; + } - const newConfig = { - blogDir: String(blogDir), - author: String(author), - publishedAtKey: String(publishedAtKey), - modifiedAtKey: String(modifiedAtKey), - authorKey: String(authorKey), - draftKey: String(draftKey), - descriptionKey: String(descriptionKey), - tagsKey: String(tagsKey), - titleKey: String(titleKey), - }; + const newConfig = { + blogDir: String(blogDir), + author: String(author), + publishedAtKey: String(publishedAtKey), + modifiedAtKey: String(modifiedAtKey), + authorKey: String(authorKey), + draftKey: String(draftKey), + descriptionKey: String(descriptionKey), + tagsKey: String(tagsKey), + titleKey: String(titleKey), + }; - await pfs.write("./mkd.json", JSON.stringify(newConfig, null, 2) + "\n"); + await pfs.write("./mkd.json", JSON.stringify(newConfig, null, 2) + "\n"); - outro("Configuration saved to mkd.json"); - }, - }); + outro("Configuration saved to mkd.json"); + }, + }); } diff --git a/src/commands/new.ts b/src/commands/new.ts index e903dc1..880d8ab 100644 --- a/src/commands/new.ts +++ b/src/commands/new.ts @@ -1,89 +1,89 @@ -import { command, restPositionals, string } from "cmd-ts"; import path from "node:path"; +import { intro, isCancel, outro, text } from "@clack/prompts"; import slugify from "@sindresorhus/slugify"; +import { command, restPositionals, string } from "cmd-ts"; import filenamify from "filenamify"; import matter from "gray-matter"; -import { isCancel, text, intro, outro } from "@clack/prompts"; import type { Config, Deps } from "../lib/deps"; export function makeNewCommand({ config, pfs }: Deps) { - return command({ - name: "new", - description: "Create a new post", - args: { - new: restPositionals({ - type: string, - displayName: "file", - description: "name of the new post", - }), - }, - handler: async ({ new: titleArray }) => { - intro("Create a new post"); - let title: string; - if (titleArray.length === 0) { - title = await generateTitle(); - if (title === "") { - return; - } - } else { - title = titleArray.join(" "); - } - const slug = slugify(title); - const fileName = filenamify(slug); - const frontmatter = await generateFrontmatter(title, config); - const filePath = path.join(config.blogDir, `${fileName}.md`); + return command({ + name: "new", + description: "Create a new post", + args: { + new: restPositionals({ + type: string, + displayName: "file", + description: "name of the new post", + }), + }, + handler: async ({ new: titleArray }) => { + intro("Create a new post"); + let title: string; + if (titleArray.length === 0) { + title = await generateTitle(); + if (title === "") { + return; + } + } else { + title = titleArray.join(" "); + } + const slug = slugify(title); + const fileName = filenamify(slug); + const frontmatter = await generateFrontmatter(title, config); + const filePath = path.join(config.blogDir, `${fileName}.md`); - await pfs.write(filePath, frontmatter); - }, - }); + await pfs.write(filePath, frontmatter); + }, + }); } async function generateFrontmatter( - title: string, - config: Config, + title: string, + config: Config, ): Promise { - const description = await makeDescription(title); - // const now = new Date(); + const description = await makeDescription(title); + // const now = new Date(); - const data = { - [config.titleKey]: title, - [config.publishedAtKey]: new Date("3000-01-01T00:00:00Z"), - [config.authorKey]: config.author, - [config.draftKey]: true, - [config.descriptionKey]: description, - [config.tagsKey]: [], - }; + const data = { + [config.titleKey]: title, + [config.publishedAtKey]: new Date("3000-01-01T00:00:00Z"), + [config.authorKey]: config.author, + [config.draftKey]: true, + [config.descriptionKey]: description, + [config.tagsKey]: [], + }; - return matter.stringify("", data); + return matter.stringify("", data); } async function makeDescription(title: string): Promise { - const description = await text({ - message: "Enter a description for the post:", - defaultValue: title, - }); + const description = await text({ + message: "Enter a description for the post:", + defaultValue: title, + }); - if (isCancel(description)) { - return ""; - } + if (isCancel(description)) { + return ""; + } - return description; + return description; } async function generateTitle(): Promise { - const title = await text({ - message: "Enter a title for the post:", - validate: (value) => { - if (!value) { - return "Title required"; - } - return undefined; - }, - }); + const title = await text({ + message: "Enter a title for the post:", + validate: (value) => { + if (!value) { + return "Title required"; + } + return undefined; + }, + }); - if (isCancel(title)) { - return ""; - } + if (isCancel(title)) { + return ""; + } - return title; + return title; } diff --git a/src/commands/publish.ts b/src/commands/publish.ts index 8cc7a09..e55f6b0 100644 --- a/src/commands/publish.ts +++ b/src/commands/publish.ts @@ -1,66 +1,66 @@ +import { autocomplete, intro, isCancel, outro } from "@clack/prompts"; import { command } from "cmd-ts"; -import { isCancel, intro, outro, autocomplete } from "@clack/prompts"; import matter from "gray-matter"; -import type { Config, Deps } from "../lib/deps"; import { - readPosts, - parseFrontmatter, - postsToOptions, - selectedValuesToPosts, - Post, + type Post, + parseFrontmatter, + postsToOptions, + readPosts, + selectedValuesToPosts, } from "../lib/commands"; +import type { Config, Deps } from "../lib/deps"; export function makePublishCommand({ config, pfs }: Deps) { - return command({ - name: "publish", - description: "Undraft a post", - args: {}, - handler: async () => { - const posts = await readPosts(pfs, config); - const drafts = posts.filter( - (post) => parseFrontmatter(post)[config.draftKey] === true, - ); - const selected = await getPostsToPublish(drafts, config); + return command({ + name: "publish", + description: "Undraft a post", + args: {}, + handler: async () => { + const posts = await readPosts(pfs, config); + const drafts = posts.filter( + (post) => parseFrontmatter(post)[config.draftKey] === true, + ); + const selected = await getPostsToPublish(drafts, config); - for (const draft of selected) { - await updateDraftFrontMatter(draft, { config, pfs }); - } - }, - }); + for (const draft of selected) { + await updateDraftFrontMatter(draft, { config, pfs }); + } + }, + }); } async function getPostsToPublish( - drafts: Post[], - config: Config, + drafts: Post[], + config: Config, ): Promise { - const options = postsToOptions(drafts, config); - intro("Publishing posts"); + const options = postsToOptions(drafts, config); + intro("Publishing posts"); - const selected = await autocomplete({ - message: "Select post to publish", - options, - }); + const selected = await autocomplete({ + message: "Select post to publish", + options, + }); - if (isCancel(selected)) { - outro("Publishing cancelled."); - return []; - } + if (isCancel(selected)) { + outro("Publishing cancelled."); + return []; + } - outro("Posts undrafted..."); + outro("Posts undrafted..."); - return selectedValuesToPosts( - Array.isArray(selected) ? selected : [selected], - drafts, - ); + return selectedValuesToPosts( + Array.isArray(selected) ? selected : [selected], + drafts, + ); } async function updateDraftFrontMatter(draft: Post, deps: Deps) { - const now = new Date(); - const parsed = matter(draft.content); - const fm = parsed.data as Record; - fm[deps.config.draftKey] = false; - fm[deps.config.publishedAtKey] = now; - const updatedContent = matter.stringify(parsed.content, fm); - await deps.pfs.write(`${deps.config.blogDir}/${draft.file}`, updatedContent); - return { ...draft, content: updatedContent }; + const now = new Date(); + const parsed = matter(draft.content); + const fm = parsed.data as Record; + fm[deps.config.draftKey] = false; + fm[deps.config.publishedAtKey] = now; + const updatedContent = matter.stringify(parsed.content, fm); + await deps.pfs.write(`${deps.config.blogDir}/${draft.file}`, updatedContent); + return { ...draft, content: updatedContent }; } diff --git a/src/commands/unpublish.ts b/src/commands/unpublish.ts index 89988f9..58df406 100644 --- a/src/commands/unpublish.ts +++ b/src/commands/unpublish.ts @@ -1,59 +1,59 @@ +import { autocomplete, intro, isCancel, outro } from "@clack/prompts"; import { command } from "cmd-ts"; -import { autocomplete, isCancel, intro, outro } from "@clack/prompts"; import matter from "gray-matter"; -import type { Config, Deps } from "../lib/deps"; import { - readPosts, - postsToOptions, - findPostByFile, - Post, + findPostByFile, + type Post, + postsToOptions, + readPosts, } from "../lib/commands"; +import type { Config, Deps } from "../lib/deps"; type Frontmatter = Record; export function makeUnPublishCommand({ config, pfs }: Deps) { - return command({ - name: "unpublish", - description: "Unpublish a post", - args: {}, - handler: async () => { - const posts = await readPosts(pfs, config); - const post = await getPostToUnpub(posts, config); - if (!post) return; - - await unpubPost(post, { config, pfs }); - }, - }); + return command({ + name: "unpublish", + description: "Unpublish a post", + args: {}, + handler: async () => { + const posts = await readPosts(pfs, config); + const post = await getPostToUnpub(posts, config); + if (!post) return; + + await unpubPost(post, { config, pfs }); + }, + }); } async function getPostToUnpub( - posts: Post[], - config: Config, + posts: Post[], + config: Config, ): Promise { - const options = postsToOptions(posts, config); + const options = postsToOptions(posts, config); - intro("Unpublish a post"); + intro("Unpublish a post"); - const selected = await autocomplete({ - message: "Select post to unpublish", - options, - }); + const selected = await autocomplete({ + message: "Select post to unpublish", + options, + }); - if (isCancel(selected)) { - outro("Unpublish cancelled."); - return null; - } - outro("Post unpublished."); + if (isCancel(selected)) { + outro("Unpublish cancelled."); + return null; + } + outro("Post unpublished."); - const found = findPostByFile(posts, selected as string); - return found ?? null; + const found = findPostByFile(posts, selected as string); + return found ?? null; } async function unpubPost(post: Post, deps: Deps) { - const parsed = matter(post.content); - const fm = parsed.data as Record; - fm[deps.config.draftKey] = true; - const updatedContent = matter.stringify(parsed.content, fm); - await deps.pfs.write(`${deps.config.blogDir}/${post.file}`, updatedContent); - return { ...post, content: updatedContent }; + const parsed = matter(post.content); + const fm = parsed.data as Record; + fm[deps.config.draftKey] = true; + const updatedContent = matter.stringify(parsed.content, fm); + await deps.pfs.write(`${deps.config.blogDir}/${post.file}`, updatedContent); + return { ...post, content: updatedContent }; } diff --git a/src/commands/update.ts b/src/commands/update.ts index b120655..3900a3d 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,59 +1,59 @@ +import { autocomplete, intro, isCancel, outro } from "@clack/prompts"; import { command } from "cmd-ts"; -import { autocomplete, isCancel, intro, outro } from "@clack/prompts"; import matter from "gray-matter"; -import type { Config, Deps } from "../lib/deps"; import { - readPosts, - postsToOptions, - findPostByFile, - Post, + findPostByFile, + type Post, + postsToOptions, + readPosts, } from "../lib/commands"; +import type { Config, Deps } from "../lib/deps"; type Frontmatter = Record; export function makeUpdateCommand({ config, pfs }: Deps) { - return command({ - name: "update", - description: "Update a post's modified date", - args: {}, - handler: async () => { - const posts = await readPosts(pfs, config); - const post = await getPostToUpdate(posts, config); - if (!post) return; - - await updatePostDate(post, { config, pfs }); - }, - }); + return command({ + name: "update", + description: "Update a post's modified date", + args: {}, + handler: async () => { + const posts = await readPosts(pfs, config); + const post = await getPostToUpdate(posts, config); + if (!post) return; + + await updatePostDate(post, { config, pfs }); + }, + }); } async function getPostToUpdate( - posts: Post[], - config: Config, + posts: Post[], + config: Config, ): Promise { - const options = postsToOptions(posts, config); + const options = postsToOptions(posts, config); - intro("Update a post"); + intro("Update a post"); - const selected = await autocomplete({ - message: "Select post to update", - options, - }); + const selected = await autocomplete({ + message: "Select post to update", + options, + }); - if (isCancel(selected)) { - outro("Update cancelled."); - return null; - } - outro("Post updated."); + if (isCancel(selected)) { + outro("Update cancelled."); + return null; + } + outro("Post updated."); - const found = findPostByFile(posts, selected as string); - return found ?? null; + const found = findPostByFile(posts, selected as string); + return found ?? null; } async function updatePostDate(post: Post, deps: Deps) { - const parsed = matter(post.content); - const fm = parsed.data as Record; - fm[deps.config.modifiedAtKey] = new Date(); - const updatedContent = matter.stringify(parsed.content, fm); - await deps.pfs.write(`${deps.config.blogDir}/${post.file}`, updatedContent); - return { ...post, content: updatedContent }; + const parsed = matter(post.content); + const fm = parsed.data as Record; + fm[deps.config.modifiedAtKey] = new Date(); + const updatedContent = matter.stringify(parsed.content, fm); + await deps.pfs.write(`${deps.config.blogDir}/${post.file}`, updatedContent); + return { ...post, content: updatedContent }; } diff --git a/src/index.ts b/src/index.ts index 959d3af..484563a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,33 @@ #!/usr/bin/env node import { run, subcommands } from "cmd-ts"; import { PoweredFileSystem } from "pwd-fs"; -import { loadConfig } from "./lib/deps"; +import { makeInitCommand } from "./commands/init"; import { makeNewCommand } from "./commands/new"; import { makePublishCommand } from "./commands/publish"; -import { makeUpdateCommand } from "./commands/update"; import { makeUnPublishCommand } from "./commands/unpublish"; -import { makeInitCommand } from "./commands/init"; +import { makeUpdateCommand } from "./commands/update"; +import { loadConfig } from "./lib/deps"; async function main() { - const pfs = new PoweredFileSystem(); - const config = await loadConfig(pfs); - const deps = { config, pfs }; + const pfs = new PoweredFileSystem(); + const config = await loadConfig(pfs); + const deps = { config, pfs }; - const app = subcommands({ - name: "mkd", - cmds: { - new: makeNewCommand(deps), - publish: makePublishCommand(deps), - update: makeUpdateCommand(deps), - unpublish: makeUnPublishCommand(deps), - init: makeInitCommand(deps), - }, - }); + const app = subcommands({ + name: "mkd", + cmds: { + new: makeNewCommand(deps), + publish: makePublishCommand(deps), + update: makeUpdateCommand(deps), + unpublish: makeUnPublishCommand(deps), + init: makeInitCommand(deps), + }, + }); - await run(app, process.argv.slice(2)); + await run(app, process.argv.slice(2)); } main().catch((error) => { - console.error(error); - process.exit(1); + console.error(error); + process.exit(1); }); diff --git a/src/lib/commands.ts b/src/lib/commands.ts index f0525c1..e0590e1 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -2,51 +2,60 @@ import matter from "gray-matter"; import type { Config, Deps } from "./deps"; export type Post = { - file: string; - content: string; + file: string; + content: string; }; export type Frontmatter = Record; /** Read all files from the blog directory and return them as Post objects */ -export async function readPosts(pfs: Deps["pfs"], config: Config): Promise { - const files = (await pfs.readdir(config.blogDir)) as string[]; - const posts: Post[] = []; - - for (const file of files) { - const content = await pfs.read(`${config.blogDir}/${file}`); - posts.push({ file, content }); - } - - return posts; +export async function readPosts( + pfs: Deps["pfs"], + config: Config, +): Promise { + const files = (await pfs.readdir(config.blogDir)) as string[]; + const posts: Post[] = []; + + for (const file of files) { + const content = await pfs.read(`${config.blogDir}/${file}`); + posts.push({ file, content }); + } + + return posts; } /** Parse frontmatter from a Post */ export function parseFrontmatter(post: Post): Frontmatter { - const { data } = matter(post.content); - return data as Frontmatter; + const { data } = matter(post.content); + return data as Frontmatter; } /** Convert Posts to prompt option objects (value/label/hint) */ export function postsToOptions(posts: Post[], config: Config) { - return posts.map((post) => { - const fm = parseFrontmatter(post); - const title = String(fm[config.titleKey] ?? post.file); - - return { - value: post.file, - label: title, - hint: post.file, - }; - }); + return posts.map((post) => { + const fm = parseFrontmatter(post); + const title = String(fm[config.titleKey] ?? post.file); + + return { + value: post.file, + label: title, + hint: post.file, + }; + }); } /** Take the raw selected values returned by the prompt and return matching Post[] */ -export function selectedValuesToPosts(selected: string[] | symbol, posts: Post[]): Post[] { - if (typeof selected === "symbol") return []; - return posts.filter((p) => selected.includes(p.file)); +export function selectedValuesToPosts( + selected: string[] | symbol, + posts: Post[], +): Post[] { + if (typeof selected === "symbol") return []; + return posts.filter((p) => selected.includes(p.file)); } -export function findPostByFile(posts: Post[], fileName: string): Post | undefined { - return posts.find((p) => p.file === fileName); +export function findPostByFile( + posts: Post[], + fileName: string, +): Post | undefined { + return posts.find((p) => p.file === fileName); } diff --git a/src/lib/deps.ts b/src/lib/deps.ts index 392ee77..88da3cf 100644 --- a/src/lib/deps.ts +++ b/src/lib/deps.ts @@ -1,59 +1,59 @@ import type { PoweredFileSystem } from "pwd-fs"; export type Config = { - blogDir: string; - titleKey: string; - author: string; - publishedAtKey: string; - modifiedAtKey: string; - authorKey: string; - draftKey: string; - descriptionKey: string; - tagsKey: string; + blogDir: string; + titleKey: string; + author: string; + publishedAtKey: string; + modifiedAtKey: string; + authorKey: string; + draftKey: string; + descriptionKey: string; + tagsKey: string; }; export type Deps = { - config: Config; - pfs: PoweredFileSystem; + config: Config; + pfs: PoweredFileSystem; }; const defaultConfig: Config = { - blogDir: "./src/blog", - titleKey: "title", - author: "", - publishedAtKey: "publishedAt", - modifiedAtKey: "updatedAt", - authorKey: "author", - draftKey: "draft", - descriptionKey: "description", - tagsKey: "tags", + blogDir: "./src/blog", + titleKey: "title", + author: "", + publishedAtKey: "publishedAt", + modifiedAtKey: "updatedAt", + authorKey: "author", + draftKey: "draft", + descriptionKey: "description", + tagsKey: "tags", }; export async function loadConfig(pfs: PoweredFileSystem): Promise { - try { - const raw = await pfs.read("./mkd.json"); + try { + const raw = await pfs.read("./mkd.json"); - if (!raw.trim()) { - return defaultConfig; - } + if (!raw.trim()) { + return defaultConfig; + } - const parsed = JSON.parse(raw); + const parsed = JSON.parse(raw); - if (!isRecord(parsed)) { - return defaultConfig; - } + if (!isRecord(parsed)) { + return defaultConfig; + } - const { $schema: _schema, ...config } = parsed; + const { $schema: _schema, ...config } = parsed; - return { - ...defaultConfig, - ...config, - }; - } catch { - return defaultConfig; - } + return { + ...defaultConfig, + ...config, + }; + } catch { + return defaultConfig; + } } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); + return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/src/lib/tags.ts b/src/lib/tags.ts index f5bb63f..5710ee6 100644 --- a/src/lib/tags.ts +++ b/src/lib/tags.ts @@ -2,9 +2,9 @@ import matter from "gray-matter"; import type { Deps } from "./deps"; export type GetAllTagsOptions = { - concurrency?: number; // how many files to parse in parallel - lowerCase?: boolean; // normalize tags to lowercase - sort?: boolean; // sort result + concurrency?: number; // how many files to parse in parallel + lowerCase?: boolean; // normalize tags to lowercase + sort?: boolean; // sort result }; /** @@ -15,66 +15,79 @@ export type GetAllTagsOptions = { * instead of `path.join` so it respects the pfs instance's `pwd` resolution semantics. */ export async function getAllTags( - deps: Deps, - opts: GetAllTagsOptions = {}, + deps: Deps, + opts: GetAllTagsOptions = {}, ): Promise { - const { pfs, config } = deps; - const { concurrency = 8, lowerCase = true, sort = true } = opts; + const { pfs, config } = deps; + const { concurrency = 8, lowerCase = true, sort = true } = opts; - const files = (await pfs.readdir(config.blogDir)) as string[]; - const mdFiles = files.filter((f) => f.endsWith(".md") || f.endsWith(".markdown")); + const files = (await pfs.readdir(config.blogDir)) as string[]; + const mdFiles = files.filter( + (f) => f.endsWith(".md") || f.endsWith(".markdown"), + ); - // split into batches of size `concurrency` - const batches: string[][] = []; - for (let i = 0; i < mdFiles.length; i += concurrency) { - batches.push(mdFiles.slice(i, i + concurrency)); - } + // split into batches of size `concurrency` + const batches: string[][] = []; + for (let i = 0; i < mdFiles.length; i += concurrency) { + batches.push(mdFiles.slice(i, i + concurrency)); + } - const tagSet = new Set(); + const tagSet = new Set(); - async function parseFile(file: string): Promise { - try { - // Resolve using pfs so it respects the base directory configured on the instance - const filePath = pfs.resolve(`${config.blogDir}/${file}`); - const content = await pfs.read(filePath); - const { data } = matter(content); - const raw = (data as Record)[config.tagsKey]; - return parseRawTags(raw); - } catch { - // ignore single-file failures - return []; - } - } + async function parseFile(file: string): Promise { + try { + // Resolve using pfs so it respects the base directory configured on the instance + const filePath = pfs.resolve(`${config.blogDir}/${file}`); + const content = await pfs.read(filePath); + const { data } = matter(content); + const raw = (data as Record)[config.tagsKey]; + return parseRawTags(raw); + } catch { + // ignore single-file failures + return []; + } + } - for (const batch of batches) { - const results = await Promise.all(batch.map(parseFile)); - for (const tags of results) { - for (let t of tags) { - if (!t) continue; - t = t.trim(); - if (!t) continue; - if (lowerCase) t = t.toLowerCase(); - tagSet.add(t); - } - } - } + for (const batch of batches) { + const results = await Promise.all(batch.map(parseFile)); + for (const tags of results) { + for (let t of tags) { + if (!t) continue; + t = t.trim(); + if (!t) continue; + if (lowerCase) t = t.toLowerCase(); + tagSet.add(t); + } + } + } - const out = Array.from(tagSet); - if (sort) out.sort(); - return out; + const out = Array.from(tagSet); + if (sort) out.sort(); + return out; } function parseRawTags(raw: unknown): string[] { - if (!raw) return []; - if (Array.isArray(raw)) return raw.map(String).flatMap(splitOnSeparators).map((s) => s.trim()).filter(Boolean); - if (typeof raw === "string") return splitOnSeparators(raw).map((s) => s.trim()).filter(Boolean); - return []; + if (!raw) return []; + if (Array.isArray(raw)) + return raw + .map(String) + .flatMap(splitOnSeparators) + .map((s) => s.trim()) + .filter(Boolean); + if (typeof raw === "string") + return splitOnSeparators(raw) + .map((s) => s.trim()) + .filter(Boolean); + return []; } function splitOnSeparators(s: string): string[] { - // Accept comma separated values and newline-separated lists, plus plain single tag strings - if (s.includes(",") || s.includes("\n")) { - return s.split(/[,\n]+/).map((x) => x.trim()).filter(Boolean); - } - return [s]; + // Accept comma separated values and newline-separated lists, plus plain single tag strings + if (s.includes(",") || s.includes("\n")) { + return s + .split(/[,\n]+/) + .map((x) => x.trim()) + .filter(Boolean); + } + return [s]; } diff --git a/tsconfig.json b/tsconfig.json index 776a797..07a1c9f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,24 @@ { - "compilerOptions": { - "lib": ["ES2022", "DOM"], - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "sourceMap": true, - "strict": true, - "skipLibCheck": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "types": ["node"], - "ignoreDeprecations": "6.0", - }, - "include": ["src/**/*"], + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "types": ["node"], + "ignoreDeprecations": "6.0" + }, + "include": ["src/**/*"] } diff --git a/tsdown.config.ts b/tsdown.config.ts index ecc98db..eada945 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -2,11 +2,11 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ["src/index.ts"], // adjust to your entry(s) - format: ["esm", "cjs"], - dts: true, // emits .d.ts - sourcemap: true, - clean: true, - target: "node16", - platform: "node", + entry: ["src/index.ts"], // adjust to your entry(s) + format: ["esm", "cjs"], + dts: true, // emits .d.ts + sourcemap: true, + clean: true, + target: "node16", + platform: "node", }); -- 2.51.2