From 2db28fc829b9731a1e1d00866ef5251a1170ac60 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 10:45:42 -0400 Subject: [PATCH 01/12] Extract the site engine into a starpod Astro integration package (spike) Monorepo spike: packages/starpod is now an installable Astro integration that injects every route, adds the preact/sitemap/tailwind/rehype wiring, exposes the user's config via virtual:starpod/config, and applies the Vercel markdown content negotiation in astro:build:done. The root site consumes it via workspace:*. Co-Authored-By: Claude Fable 5 --- astro.config.mjs | 65 +------- db/seed.ts | 9 +- drizzle.config.ts | 2 +- package.json | 22 +-- packages/starpod/package.json | 57 +++++++ .../src}/components/AdPackageCard.astro | 0 .../starpod/src}/components/Breadcrumbs.astro | 0 .../starpod/src}/components/ContactForm.tsx | 0 .../starpod/src}/components/Dots.astro | 0 .../starpod/src}/components/EpisodeList.astro | 0 .../starpod/src}/components/FormattedDate.tsx | 0 .../src}/components/FullPlayButton.tsx | 0 .../starpod/src}/components/Hosts.astro | 2 +- .../starpod/src}/components/InfoCard.astro | 0 .../src}/components/LargePlatforms.astro | 2 +- .../src}/components/NotFoundContent.astro | 0 .../starpod/src}/components/Platforms.astro | 2 +- .../starpod/src}/components/Player.tsx | 0 .../starpod/src}/components/SearchButton.tsx | 0 .../starpod/src}/components/SearchDialog.tsx | 0 .../starpod/src}/components/ShowArtwork.astro | 0 .../episode/CreatorsAndGuests.astro | 0 .../components/episode/MarkdownTranscript.tsx | 0 .../src}/components/episode/Sponsors.astro | 0 .../src}/components/episode/Transcript.tsx | 0 .../illustrations/404Illustration.astro | 0 .../illustrations/UFOIllustration.astro | 0 .../components/player/ForwardButton/index.tsx | 0 .../player/ForwardButton/styles.css | 0 .../components/player/MuteButton/index.tsx | 0 .../components/player/MuteButton/styles.css | 0 .../src}/components/player/PlayButton.tsx | 0 .../components/player/PlaybackRateButton.tsx | 0 .../components/player/RewindButton/index.tsx | 0 .../components/player/RewindButton/styles.css | 0 .../src}/components/player/Slider/index.tsx | 0 .../src}/components/player/Slider/styles.css | 0 .../starpod/src}/components/state.ts | 0 packages/starpod/src/config.ts | 5 + packages/starpod/src/content.ts | 18 +++ {db => packages/starpod/src/db}/index.ts | 0 {db => packages/starpod/src/db}/schema.ts | 0 packages/starpod/src/index.ts | 148 ++++++++++++++++++ .../starpod/src}/layouts/Layout.astro | 2 +- .../starpod/src}/lib/api-errors.ts | 0 {src => packages/starpod/src}/lib/llms.ts | 0 .../starpod/src}/lib/not-found.ts | 0 {src => packages/starpod/src}/lib/openapi.ts | 0 .../src}/lib/optimize-episode-image.ts | 0 .../lib/rehype-transcript-timestamps.d.mts | 4 + .../src}/lib/rehype-transcript-timestamps.mjs | 0 {src => packages/starpod/src}/lib/rss.ts | 22 ++- .../starpod/src}/lib/standardSite.ts | 0 .../starpod/src}/lib/transcript.ts | 0 .../.well-known/site.standard.publication.ts | 0 {src => packages/starpod/src}/pages/404.astro | 0 .../starpod/src}/pages/[...notFound].astro | 0 .../starpod/src}/pages/[episode].astro | 4 +- .../starpod/src}/pages/[episode].html.md.ts | 2 +- .../starpod/src}/pages/about.astro | 2 +- .../starpod/src}/pages/about.html.md.ts | 2 +- .../starpod/src}/pages/api/contact.ts | 0 .../src}/pages/api/episodes/[page].json.ts | 0 .../src}/pages/api/episodes/search.json.ts | 0 .../starpod/src}/pages/contact.astro | 0 .../src}/pages/episodes-index.html.md.ts | 0 .../starpod/src}/pages/for-llms.astro | 2 +- .../starpod/src}/pages/for-llms.html.md.ts | 2 +- .../starpod/src}/pages/index.astro | 0 .../starpod/src}/pages/llms.txt.ts | 2 +- .../starpod/src}/pages/openapi.json.ts | 2 +- .../starpod/src}/pages/robots.txt.ts | 0 .../starpod/src}/pages/sponsor.astro | 0 .../starpod/src}/styles/buttons.css | 0 .../starpod/src}/styles/global.css | 0 .../starpod/src}/styles/gradient-icon.css | 0 .../starpod/src}/styles/search-icon.css | 0 .../starpod/src}/styles/tailwind.css | 9 ++ .../starpod/src}/svgs/404-dark.svg | 0 .../starpod/src}/svgs/404-light.svg | 0 {src => packages/starpod/src}/svgs/apple.svg | 0 .../starpod/src}/svgs/overcast.svg | 0 .../starpod/src}/svgs/pocket-casts.svg | 0 .../starpod/src}/svgs/spotify.svg | 0 .../starpod/src}/svgs/ufo-dark.svg | 0 .../starpod/src}/svgs/ufo-light.svg | 0 .../starpod/src}/svgs/youtube.svg | 0 {src => packages/starpod/src}/utils/config.ts | 0 .../starpod/src}/utils/dasherize.ts | 0 .../starpod/src}/utils/truncate.ts | 0 .../starpod/src/vercel-md-negotiation.d.mts | 16 ++ .../starpod/src}/vercel-md-negotiation.mjs | 0 packages/starpod/src/virtual.d.ts | 4 + pnpm-lock.yaml | 118 +++++++++----- pnpm-workspace.yaml | 2 + scripts/publish-episodes.ts | 2 +- src/content.config.ts | 6 +- src/content/config.ts | 0 starpod.config.ts | 2 +- tests/unit/ForwardButton.test.tsx | 2 +- tests/unit/FullPlayButton.test.tsx | 4 +- tests/unit/MuteButton.test.tsx | 4 +- tests/unit/PlayButton.test.tsx | 4 +- tests/unit/PlaybackRateButton.test.tsx | 2 +- tests/unit/Player.test.tsx | 4 +- tests/unit/RewindButton.test.tsx | 2 +- tests/unit/SearchButton.test.tsx | 4 +- tests/unit/SearchDialog.test.tsx | 4 +- tests/unit/Slider.test.tsx | 2 +- tests/unit/contact-api.test.ts | 2 +- tests/unit/llms.test.ts | 6 +- tests/unit/not-found.test.ts | 4 +- tests/unit/openapi.test.ts | 6 +- .../unit/rehype-transcript-timestamps.test.ts | 2 +- tests/unit/robots.test.ts | 2 +- tests/unit/transcript.test.ts | 4 +- tests/unit/vercel-md-negotiation.test.ts | 2 +- vitest.config.ts | 9 +- 118 files changed, 428 insertions(+), 178 deletions(-) create mode 100644 packages/starpod/package.json rename {src => packages/starpod/src}/components/AdPackageCard.astro (100%) rename {src => packages/starpod/src}/components/Breadcrumbs.astro (100%) rename {src => packages/starpod/src}/components/ContactForm.tsx (100%) rename {src => packages/starpod/src}/components/Dots.astro (100%) rename {src => packages/starpod/src}/components/EpisodeList.astro (100%) rename {src => packages/starpod/src}/components/FormattedDate.tsx (100%) rename {src => packages/starpod/src}/components/FullPlayButton.tsx (100%) rename {src => packages/starpod/src}/components/Hosts.astro (93%) rename {src => packages/starpod/src}/components/InfoCard.astro (100%) rename {src => packages/starpod/src}/components/LargePlatforms.astro (98%) rename {src => packages/starpod/src}/components/NotFoundContent.astro (100%) rename {src => packages/starpod/src}/components/Platforms.astro (96%) rename {src => packages/starpod/src}/components/Player.tsx (100%) rename {src => packages/starpod/src}/components/SearchButton.tsx (100%) rename {src => packages/starpod/src}/components/SearchDialog.tsx (100%) rename {src => packages/starpod/src}/components/ShowArtwork.astro (100%) rename {src => packages/starpod/src}/components/episode/CreatorsAndGuests.astro (100%) rename {src => packages/starpod/src}/components/episode/MarkdownTranscript.tsx (100%) rename {src => packages/starpod/src}/components/episode/Sponsors.astro (100%) rename {src => packages/starpod/src}/components/episode/Transcript.tsx (100%) rename {src => packages/starpod/src}/components/illustrations/404Illustration.astro (100%) rename {src => packages/starpod/src}/components/illustrations/UFOIllustration.astro (100%) rename {src => packages/starpod/src}/components/player/ForwardButton/index.tsx (100%) rename {src => packages/starpod/src}/components/player/ForwardButton/styles.css (100%) rename {src => packages/starpod/src}/components/player/MuteButton/index.tsx (100%) rename {src => packages/starpod/src}/components/player/MuteButton/styles.css (100%) rename {src => packages/starpod/src}/components/player/PlayButton.tsx (100%) rename {src => packages/starpod/src}/components/player/PlaybackRateButton.tsx (100%) rename {src => packages/starpod/src}/components/player/RewindButton/index.tsx (100%) rename {src => packages/starpod/src}/components/player/RewindButton/styles.css (100%) rename {src => packages/starpod/src}/components/player/Slider/index.tsx (100%) rename {src => packages/starpod/src}/components/player/Slider/styles.css (100%) rename {src => packages/starpod/src}/components/state.ts (100%) create mode 100644 packages/starpod/src/config.ts create mode 100644 packages/starpod/src/content.ts rename {db => packages/starpod/src/db}/index.ts (100%) rename {db => packages/starpod/src/db}/schema.ts (100%) create mode 100644 packages/starpod/src/index.ts rename {src => packages/starpod/src}/layouts/Layout.astro (99%) rename {src => packages/starpod/src}/lib/api-errors.ts (100%) rename {src => packages/starpod/src}/lib/llms.ts (100%) rename {src => packages/starpod/src}/lib/not-found.ts (100%) rename {src => packages/starpod/src}/lib/openapi.ts (100%) rename {src => packages/starpod/src}/lib/optimize-episode-image.ts (100%) create mode 100644 packages/starpod/src/lib/rehype-transcript-timestamps.d.mts rename {src => packages/starpod/src}/lib/rehype-transcript-timestamps.mjs (100%) rename {src => packages/starpod/src}/lib/rss.ts (84%) rename {src => packages/starpod/src}/lib/standardSite.ts (100%) rename {src => packages/starpod/src}/lib/transcript.ts (100%) rename {src => packages/starpod/src}/pages/.well-known/site.standard.publication.ts (100%) rename {src => packages/starpod/src}/pages/404.astro (100%) rename {src => packages/starpod/src}/pages/[...notFound].astro (100%) rename {src => packages/starpod/src}/pages/[episode].astro (99%) rename {src => packages/starpod/src}/pages/[episode].html.md.ts (96%) rename {src => packages/starpod/src}/pages/about.astro (97%) rename {src => packages/starpod/src}/pages/about.html.md.ts (88%) rename {src => packages/starpod/src}/pages/api/contact.ts (100%) rename {src => packages/starpod/src}/pages/api/episodes/[page].json.ts (100%) rename {src => packages/starpod/src}/pages/api/episodes/search.json.ts (100%) rename {src => packages/starpod/src}/pages/contact.astro (100%) rename {src => packages/starpod/src}/pages/episodes-index.html.md.ts (100%) rename {src => packages/starpod/src}/pages/for-llms.astro (99%) rename {src => packages/starpod/src}/pages/for-llms.html.md.ts (90%) rename {src => packages/starpod/src}/pages/index.astro (100%) rename {src => packages/starpod/src}/pages/llms.txt.ts (92%) rename {src => packages/starpod/src}/pages/openapi.json.ts (89%) rename {src => packages/starpod/src}/pages/robots.txt.ts (100%) rename {src => packages/starpod/src}/pages/sponsor.astro (100%) rename {src => packages/starpod/src}/styles/buttons.css (100%) rename {src => packages/starpod/src}/styles/global.css (100%) rename {src => packages/starpod/src}/styles/gradient-icon.css (100%) rename {src => packages/starpod/src}/styles/search-icon.css (100%) rename {src => packages/starpod/src}/styles/tailwind.css (87%) rename {src => packages/starpod/src}/svgs/404-dark.svg (100%) rename {src => packages/starpod/src}/svgs/404-light.svg (100%) rename {src => packages/starpod/src}/svgs/apple.svg (100%) rename {src => packages/starpod/src}/svgs/overcast.svg (100%) rename {src => packages/starpod/src}/svgs/pocket-casts.svg (100%) rename {src => packages/starpod/src}/svgs/spotify.svg (100%) rename {src => packages/starpod/src}/svgs/ufo-dark.svg (100%) rename {src => packages/starpod/src}/svgs/ufo-light.svg (100%) rename {src => packages/starpod/src}/svgs/youtube.svg (100%) rename {src => packages/starpod/src}/utils/config.ts (100%) rename {src => packages/starpod/src}/utils/dasherize.ts (100%) rename {src => packages/starpod/src}/utils/truncate.ts (100%) create mode 100644 packages/starpod/src/vercel-md-negotiation.d.mts rename {scripts => packages/starpod/src}/vercel-md-negotiation.mjs (100%) create mode 100644 packages/starpod/src/virtual.d.ts create mode 100644 pnpm-workspace.yaml delete mode 100644 src/content/config.ts diff --git a/astro.config.mjs b/astro.config.mjs index cc4fb4f1..90b22e00 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,10 +1,8 @@ -import { defineConfig, fontProviders } from 'astro/config'; -import preact from '@astrojs/preact'; -import sitemap from '@astrojs/sitemap'; -import tailwindcss from '@tailwindcss/vite'; import vercel from '@astrojs/vercel'; +import { defineConfig } from 'astro/config'; +import starpod from 'starpod'; -import rehypeTranscriptTimestamps from './src/lib/rehype-transcript-timestamps.mjs'; +import starpodConfig from './starpod.config'; // https://astro.build/config export default defineConfig({ @@ -28,60 +26,8 @@ export default defineConfig({ enabled: true } }), - build: { - inlineStylesheets: 'always' - }, - markdown: { - // Makes bracketed timestamps in markdown transcripts clickable for seeking. - rehypePlugins: [rehypeTranscriptTimestamps] - }, - experimental: { - clientPrerender: true - }, - fonts: [ - { - provider: fontProviders.google(), - name: 'Inter', - cssVariable: '--astro-font-inter', - formats: ['woff2'], - styles: ['normal'], - subsets: ['latin'], - weights: ['300 900'], - options: { - experimental: { - variableAxis: { - opsz: ['14..32'] - } - } - } - } - ], - image: { - remotePatterns: [ - { - protocol: 'https' - }, - { - protocol: 'http' - } - ] - }, - prefetch: { - prefetchAll: true, - defaultStrategy: 'viewport' - }, site: 'https://whiskey.fm', - trailingSlash: 'never', - integrations: [ - preact(), - sitemap({ - filter: (page) => { - const pathname = new URL(page).pathname; - // Exclude episode number pages and only include slug pages. - return !/^\/\d+\/?$/.test(pathname); - } - }) - ], + integrations: [starpod(starpodConfig)], // These were specific redirects we needed for our podcast, if you do not have any routes to redirect, you can safely remove this. redirects: { '/hot-takes-tan-stack-and-open-source-with-tanner-linsley': @@ -90,8 +36,5 @@ export default defineConfig({ 'creating-codepen-tackling-tailwind-and-keeping-it-simple-with-chris-coyier', '/coding-languages-ai-and-the-evolution-of-game-development-with-phillip-winston': '/coding-languages-ai-and-the-evolution-of-game-development-with-philip-winston' - }, - vite: { - plugins: [tailwindcss()] } }); diff --git a/db/seed.ts b/db/seed.ts index 68c4f539..d0d80167 100644 --- a/db/seed.ts +++ b/db/seed.ts @@ -2,16 +2,17 @@ import 'dotenv/config'; import { sql } from 'drizzle-orm'; -import { createDb } from './index'; +import { createDb } from 'starpod/db'; import { Episode, HostOrGuest, Person, Sponsor, SponsorForEpisode -} from './schema'; +} from 'starpod/db/schema'; +import { getAllEpisodes } from 'starpod/src/lib/rss'; -import { getAllEpisodes } from '../src/lib/rss'; +import starpodConfig from '../starpod.config'; import people from './data/people'; import peoplePerEpisode from './data/people-per-episode'; import sponsors from './data/sponsors'; @@ -43,7 +44,7 @@ async function seed() { } }); - const allEpisodes = await getAllEpisodes(); + const allEpisodes = await getAllEpisodes(starpodConfig); const episodes = allEpisodes.map((episode) => { return { episodeSlug: episode.episodeSlug diff --git a/drizzle.config.ts b/drizzle.config.ts index 4361179f..f1e8d184 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'drizzle-kit'; export default defineConfig({ - schema: './db/schema.ts', + schema: './packages/starpod/src/db/schema.ts', out: './drizzle', dialect: 'turso', dbCredentials: { diff --git a/package.json b/package.json index 948d2c87..0b451fff 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { - "name": "starpod", + "name": "starpod-site", + "private": true, "type": "module", "version": "0.0.1", "repository": { @@ -9,7 +10,7 @@ "license": "MIT", "scripts": { "astro": "astro", - "build": "astro check && astro build && node scripts/vercel-md-negotiation.mjs", + "build": "astro check && astro build", "db:push": "drizzle-kit push", "db:seed": "tsx db/seed.ts", "db:studio": "drizzle-kit studio", @@ -25,30 +26,19 @@ "test:unit": "vitest" }, "dependencies": { - "@astrojs/preact": "^5.1.4", "@astrojs/vercel": "^10.0.8", "@bryanguffey/astro-standard-site": "^1.0.3", - "@libsql/client": "^0.17.3", - "@preact/signals": "^2.9.1", - "@vercel/analytics": "^1.6.1", - "@vercel/speed-insights": "^1.3.1", "astro": "6.4.2", - "astro-seo-schema": "^5.2.0", - "atropos": "^2.0.2", "drizzle-orm": "^0.45.2", - "preact": "^10.29.2", + "html-to-text": "^9.0.5", "rss-to-json": "^2.1.1", - "schema-dts": "^1.1.5", + "starpod": "workspace:*", "valibot": "^1.4.1" }, "devDependencies": { "@astrojs/check": "^0.9.9", - "@astrojs/sitemap": "^3.7.3", "@eslint/js": "^9.39.4", "@playwright/test": "^1.60.0", - "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.3.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/preact": "^3.2.4", "@types/html-to-text": "^9.0.4", @@ -60,8 +50,8 @@ "eslint": "^9.39.4", "eslint-plugin-astro": "^1.7.0", "globals": "^16.5.0", - "html-to-text": "^9.0.5", "jsdom": "^27.4.0", + "preact": "^10.29.2", "prettier": "^3.8.3", "prettier-plugin-astro": "^0.14.1", "prettier-plugin-tailwindcss": "^0.7.4", diff --git a/packages/starpod/package.json b/packages/starpod/package.json new file mode 100644 index 00000000..61d2188c --- /dev/null +++ b/packages/starpod/package.json @@ -0,0 +1,57 @@ +{ + "name": "starpod", + "type": "module", + "version": "0.1.0", + "description": "Turn an RSS feed into a full podcast website. An Astro integration.", + "repository": { + "type": "git", + "url": "git+https://github.com/shipshapecode/starpod.git", + "directory": "packages/starpod" + }, + "license": "MIT", + "keywords": [ + "astro-integration", + "astro-component", + "withastro", + "podcast" + ], + "exports": { + ".": "./src/index.ts", + "./config": "./src/config.ts", + "./content": "./src/content.ts", + "./db": "./src/db/index.ts", + "./db/schema": "./src/db/schema.ts", + "./src/*": "./src/*" + }, + "dependencies": { + "@astrojs/preact": "^5.1.4", + "@astrojs/sitemap": "^3.7.3", + "@bryanguffey/astro-standard-site": "^1.0.3", + "@libsql/client": "^0.17.3", + "@preact/signals": "^2.9.1", + "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.3.0", + "@vercel/speed-insights": "^1.3.1", + "astro-seo-schema": "^5.2.0", + "atropos": "^2.0.2", + "drizzle-orm": "^0.45.2", + "html-to-text": "^9.0.5", + "preact": "^10.29.2", + "rss-to-json": "^2.1.1", + "schema-dts": "^1.1.5", + "tailwindcss": "^4.3.0", + "valibot": "^1.4.1" + }, + "peerDependencies": { + "astro": "^6.0.0" + }, + "devDependencies": { + "@types/html-to-text": "^9.0.4", + "astro": "6.4.2", + "typescript": "^5.9.3" + }, + "engines": { + "node": "^22.0.0" + } +} diff --git a/src/components/AdPackageCard.astro b/packages/starpod/src/components/AdPackageCard.astro similarity index 100% rename from src/components/AdPackageCard.astro rename to packages/starpod/src/components/AdPackageCard.astro diff --git a/src/components/Breadcrumbs.astro b/packages/starpod/src/components/Breadcrumbs.astro similarity index 100% rename from src/components/Breadcrumbs.astro rename to packages/starpod/src/components/Breadcrumbs.astro diff --git a/src/components/ContactForm.tsx b/packages/starpod/src/components/ContactForm.tsx similarity index 100% rename from src/components/ContactForm.tsx rename to packages/starpod/src/components/ContactForm.tsx diff --git a/src/components/Dots.astro b/packages/starpod/src/components/Dots.astro similarity index 100% rename from src/components/Dots.astro rename to packages/starpod/src/components/Dots.astro diff --git a/src/components/EpisodeList.astro b/packages/starpod/src/components/EpisodeList.astro similarity index 100% rename from src/components/EpisodeList.astro rename to packages/starpod/src/components/EpisodeList.astro diff --git a/src/components/FormattedDate.tsx b/packages/starpod/src/components/FormattedDate.tsx similarity index 100% rename from src/components/FormattedDate.tsx rename to packages/starpod/src/components/FormattedDate.tsx diff --git a/src/components/FullPlayButton.tsx b/packages/starpod/src/components/FullPlayButton.tsx similarity index 100% rename from src/components/FullPlayButton.tsx rename to packages/starpod/src/components/FullPlayButton.tsx diff --git a/src/components/Hosts.astro b/packages/starpod/src/components/Hosts.astro similarity index 93% rename from src/components/Hosts.astro rename to packages/starpod/src/components/Hosts.astro index dbd68b96..42b47a5d 100644 --- a/src/components/Hosts.astro +++ b/packages/starpod/src/components/Hosts.astro @@ -2,7 +2,7 @@ import type { ImageMetadata } from 'astro'; import { Image } from 'astro:assets'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; const { hosts } = starpodConfig; const images = import.meta.glob<{ default: ImageMetadata }>( diff --git a/src/components/InfoCard.astro b/packages/starpod/src/components/InfoCard.astro similarity index 100% rename from src/components/InfoCard.astro rename to packages/starpod/src/components/InfoCard.astro diff --git a/src/components/LargePlatforms.astro b/packages/starpod/src/components/LargePlatforms.astro similarity index 98% rename from src/components/LargePlatforms.astro rename to packages/starpod/src/components/LargePlatforms.astro index 631b10f1..d12242ea 100644 --- a/src/components/LargePlatforms.astro +++ b/packages/starpod/src/components/LargePlatforms.astro @@ -1,5 +1,5 @@ --- -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; import AppleIcon from '../svgs/apple.svg?raw'; import OvercastIcon from '../svgs/overcast.svg?raw'; import PocketCastsIcon from '../svgs/pocket-casts.svg?raw'; diff --git a/src/components/NotFoundContent.astro b/packages/starpod/src/components/NotFoundContent.astro similarity index 100% rename from src/components/NotFoundContent.astro rename to packages/starpod/src/components/NotFoundContent.astro diff --git a/src/components/Platforms.astro b/packages/starpod/src/components/Platforms.astro similarity index 96% rename from src/components/Platforms.astro rename to packages/starpod/src/components/Platforms.astro index 972e4299..6358f15e 100644 --- a/src/components/Platforms.astro +++ b/packages/starpod/src/components/Platforms.astro @@ -1,5 +1,5 @@ --- -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; import { getShowInfo } from '../lib/rss'; const { platforms } = starpodConfig; diff --git a/src/components/Player.tsx b/packages/starpod/src/components/Player.tsx similarity index 100% rename from src/components/Player.tsx rename to packages/starpod/src/components/Player.tsx diff --git a/src/components/SearchButton.tsx b/packages/starpod/src/components/SearchButton.tsx similarity index 100% rename from src/components/SearchButton.tsx rename to packages/starpod/src/components/SearchButton.tsx diff --git a/src/components/SearchDialog.tsx b/packages/starpod/src/components/SearchDialog.tsx similarity index 100% rename from src/components/SearchDialog.tsx rename to packages/starpod/src/components/SearchDialog.tsx diff --git a/src/components/ShowArtwork.astro b/packages/starpod/src/components/ShowArtwork.astro similarity index 100% rename from src/components/ShowArtwork.astro rename to packages/starpod/src/components/ShowArtwork.astro diff --git a/src/components/episode/CreatorsAndGuests.astro b/packages/starpod/src/components/episode/CreatorsAndGuests.astro similarity index 100% rename from src/components/episode/CreatorsAndGuests.astro rename to packages/starpod/src/components/episode/CreatorsAndGuests.astro diff --git a/src/components/episode/MarkdownTranscript.tsx b/packages/starpod/src/components/episode/MarkdownTranscript.tsx similarity index 100% rename from src/components/episode/MarkdownTranscript.tsx rename to packages/starpod/src/components/episode/MarkdownTranscript.tsx diff --git a/src/components/episode/Sponsors.astro b/packages/starpod/src/components/episode/Sponsors.astro similarity index 100% rename from src/components/episode/Sponsors.astro rename to packages/starpod/src/components/episode/Sponsors.astro diff --git a/src/components/episode/Transcript.tsx b/packages/starpod/src/components/episode/Transcript.tsx similarity index 100% rename from src/components/episode/Transcript.tsx rename to packages/starpod/src/components/episode/Transcript.tsx diff --git a/src/components/illustrations/404Illustration.astro b/packages/starpod/src/components/illustrations/404Illustration.astro similarity index 100% rename from src/components/illustrations/404Illustration.astro rename to packages/starpod/src/components/illustrations/404Illustration.astro diff --git a/src/components/illustrations/UFOIllustration.astro b/packages/starpod/src/components/illustrations/UFOIllustration.astro similarity index 100% rename from src/components/illustrations/UFOIllustration.astro rename to packages/starpod/src/components/illustrations/UFOIllustration.astro diff --git a/src/components/player/ForwardButton/index.tsx b/packages/starpod/src/components/player/ForwardButton/index.tsx similarity index 100% rename from src/components/player/ForwardButton/index.tsx rename to packages/starpod/src/components/player/ForwardButton/index.tsx diff --git a/src/components/player/ForwardButton/styles.css b/packages/starpod/src/components/player/ForwardButton/styles.css similarity index 100% rename from src/components/player/ForwardButton/styles.css rename to packages/starpod/src/components/player/ForwardButton/styles.css diff --git a/src/components/player/MuteButton/index.tsx b/packages/starpod/src/components/player/MuteButton/index.tsx similarity index 100% rename from src/components/player/MuteButton/index.tsx rename to packages/starpod/src/components/player/MuteButton/index.tsx diff --git a/src/components/player/MuteButton/styles.css b/packages/starpod/src/components/player/MuteButton/styles.css similarity index 100% rename from src/components/player/MuteButton/styles.css rename to packages/starpod/src/components/player/MuteButton/styles.css diff --git a/src/components/player/PlayButton.tsx b/packages/starpod/src/components/player/PlayButton.tsx similarity index 100% rename from src/components/player/PlayButton.tsx rename to packages/starpod/src/components/player/PlayButton.tsx diff --git a/src/components/player/PlaybackRateButton.tsx b/packages/starpod/src/components/player/PlaybackRateButton.tsx similarity index 100% rename from src/components/player/PlaybackRateButton.tsx rename to packages/starpod/src/components/player/PlaybackRateButton.tsx diff --git a/src/components/player/RewindButton/index.tsx b/packages/starpod/src/components/player/RewindButton/index.tsx similarity index 100% rename from src/components/player/RewindButton/index.tsx rename to packages/starpod/src/components/player/RewindButton/index.tsx diff --git a/src/components/player/RewindButton/styles.css b/packages/starpod/src/components/player/RewindButton/styles.css similarity index 100% rename from src/components/player/RewindButton/styles.css rename to packages/starpod/src/components/player/RewindButton/styles.css diff --git a/src/components/player/Slider/index.tsx b/packages/starpod/src/components/player/Slider/index.tsx similarity index 100% rename from src/components/player/Slider/index.tsx rename to packages/starpod/src/components/player/Slider/index.tsx diff --git a/src/components/player/Slider/styles.css b/packages/starpod/src/components/player/Slider/styles.css similarity index 100% rename from src/components/player/Slider/styles.css rename to packages/starpod/src/components/player/Slider/styles.css diff --git a/src/components/state.ts b/packages/starpod/src/components/state.ts similarity index 100% rename from src/components/state.ts rename to packages/starpod/src/components/state.ts diff --git a/packages/starpod/src/config.ts b/packages/starpod/src/config.ts new file mode 100644 index 00000000..60757f45 --- /dev/null +++ b/packages/starpod/src/config.ts @@ -0,0 +1,5 @@ +export { + defineStarpodConfig, + type Host, + type StarpodConfig +} from './utils/config'; diff --git a/packages/starpod/src/content.ts b/packages/starpod/src/content.ts new file mode 100644 index 00000000..26735502 --- /dev/null +++ b/packages/starpod/src/content.ts @@ -0,0 +1,18 @@ +import { glob } from 'astro/loaders'; + +/** + * Loader for episode transcript markdown files, named by episode number + * (e.g. `42.md`). Use in the consuming site's `src/content.config.ts`: + * + * ```ts + * import { defineCollection } from 'astro:content'; + * import { transcriptsLoader } from 'starpod/content'; + * + * export const collections = { + * transcripts: defineCollection({ loader: transcriptsLoader() }) + * }; + * ``` + */ +export function transcriptsLoader(base = './src/content/transcripts') { + return glob({ pattern: '**/*.{md,mdx}', base }); +} diff --git a/db/index.ts b/packages/starpod/src/db/index.ts similarity index 100% rename from db/index.ts rename to packages/starpod/src/db/index.ts diff --git a/db/schema.ts b/packages/starpod/src/db/schema.ts similarity index 100% rename from db/schema.ts rename to packages/starpod/src/db/schema.ts diff --git a/packages/starpod/src/index.ts b/packages/starpod/src/index.ts new file mode 100644 index 00000000..d446662d --- /dev/null +++ b/packages/starpod/src/index.ts @@ -0,0 +1,148 @@ +import { fileURLToPath } from 'node:url'; + +import preact from '@astrojs/preact'; +import sitemap from '@astrojs/sitemap'; +import tailwindcss from '@tailwindcss/vite'; +import type { AstroIntegration } from 'astro'; +import { fontProviders } from 'astro/config'; +import type { Plugin as VitePlugin } from 'vite'; + +import rehypeTranscriptTimestamps from './lib/rehype-transcript-timestamps.mjs'; +import { type StarpodConfig } from './utils/config'; +import { main as applyVercelMarkdownNegotiation } from './vercel-md-negotiation.mjs'; + +const VIRTUAL_CONFIG_ID = 'virtual:starpod/config'; +const RESOLVED_VIRTUAL_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID; + +/** + * Exposes the user's Starpod config to every page and component in the + * package as the `virtual:starpod/config` module. + */ +function starpodConfigPlugin(config: StarpodConfig): VitePlugin { + return { + name: 'starpod-config', + resolveId(id) { + if (id === VIRTUAL_CONFIG_ID) { + return RESOLVED_VIRTUAL_CONFIG_ID; + } + }, + load(id) { + if (id === RESOLVED_VIRTUAL_CONFIG_ID) { + return `export default ${JSON.stringify(config)};`; + } + } + }; +} + +/** + * Every route the integration injects, mirroring the file-based routes the + * pages had when they lived in the consuming site's `src/pages`. + */ +const ROUTES: Array<{ pattern: string; entrypoint: string }> = [ + { pattern: '/', entrypoint: 'index.astro' }, + { pattern: '/404', entrypoint: '404.astro' }, + { pattern: '/[...notFound]', entrypoint: '[...notFound].astro' }, + { pattern: '/[episode]', entrypoint: '[episode].astro' }, + { pattern: '/[episode].html.md', entrypoint: '[episode].html.md.ts' }, + { pattern: '/about', entrypoint: 'about.astro' }, + { pattern: '/about.html.md', entrypoint: 'about.html.md.ts' }, + { pattern: '/contact', entrypoint: 'contact.astro' }, + { + pattern: '/episodes-index.html.md', + entrypoint: 'episodes-index.html.md.ts' + }, + { pattern: '/for-llms', entrypoint: 'for-llms.astro' }, + { pattern: '/for-llms.html.md', entrypoint: 'for-llms.html.md.ts' }, + { pattern: '/llms.txt', entrypoint: 'llms.txt.ts' }, + { pattern: '/openapi.json', entrypoint: 'openapi.json.ts' }, + { pattern: '/robots.txt', entrypoint: 'robots.txt.ts' }, + { pattern: '/sponsor', entrypoint: 'sponsor.astro' }, + { pattern: '/api/contact', entrypoint: 'api/contact.ts' }, + { pattern: '/api/episodes/[page].json', entrypoint: 'api/episodes/[page].json.ts' }, + { + pattern: '/api/episodes/search.json', + entrypoint: 'api/episodes/search.json.ts' + } +]; + +export default function starpod(starpodConfig: StarpodConfig): AstroIntegration { + let projectRoot: URL; + + return { + name: 'starpod', + hooks: { + 'astro:config:setup': ({ config, injectRoute, updateConfig }) => { + projectRoot = config.root; + + for (const route of ROUTES) { + injectRoute({ + pattern: route.pattern, + entrypoint: new URL(`./pages/${route.entrypoint}`, import.meta.url) + }); + } + + updateConfig({ + build: { + inlineStylesheets: 'always' + }, + experimental: { + clientPrerender: true + }, + fonts: [ + { + provider: fontProviders.google(), + name: 'Inter', + cssVariable: '--astro-font-inter', + formats: ['woff2'], + styles: ['normal'], + subsets: ['latin'], + weights: ['300 900'], + options: { + experimental: { + variableAxis: { + opsz: ['14..32'] + } + } + } + } + ], + image: { + remotePatterns: [{ protocol: 'https' }, { protocol: 'http' }] + }, + integrations: [ + preact(), + sitemap({ + filter: (page) => { + const pathname = new URL(page).pathname; + // Exclude episode number pages and only include slug pages. + return !/^\/\d+\/?$/.test(pathname); + } + }) + ], + markdown: { + // Makes bracketed timestamps in markdown transcripts clickable + // for seeking. + rehypePlugins: [rehypeTranscriptTimestamps] + }, + prefetch: { + prefetchAll: true, + defaultStrategy: 'viewport' + }, + trailingSlash: 'never', + vite: { + plugins: [tailwindcss(), starpodConfigPlugin(starpodConfig)] + } + }); + }, + 'astro:build:done': () => { + // Injects `Accept: text/markdown` content-negotiation routes into the + // Vercel build output. No-ops for other adapters. + applyVercelMarkdownNegotiation( + fileURLToPath(new URL('./.vercel/output', projectRoot)) + ); + } + } + }; +} + +export { defineStarpodConfig, type Host, type StarpodConfig } from './utils/config'; diff --git a/src/layouts/Layout.astro b/packages/starpod/src/layouts/Layout.astro similarity index 99% rename from src/layouts/Layout.astro rename to packages/starpod/src/layouts/Layout.astro index fd7057e5..ffa86e4f 100644 --- a/src/layouts/Layout.astro +++ b/packages/starpod/src/layouts/Layout.astro @@ -16,7 +16,7 @@ import SearchDialog from '../components/SearchDialog'; import ShowArtwork from '../components/ShowArtwork.astro'; import { getShowInfo } from '../lib/rss'; -import starpodConfig from 'starpod.config'; +import starpodConfig from 'virtual:starpod/config'; import '../styles/global.css'; diff --git a/src/lib/api-errors.ts b/packages/starpod/src/lib/api-errors.ts similarity index 100% rename from src/lib/api-errors.ts rename to packages/starpod/src/lib/api-errors.ts diff --git a/src/lib/llms.ts b/packages/starpod/src/lib/llms.ts similarity index 100% rename from src/lib/llms.ts rename to packages/starpod/src/lib/llms.ts diff --git a/src/lib/not-found.ts b/packages/starpod/src/lib/not-found.ts similarity index 100% rename from src/lib/not-found.ts rename to packages/starpod/src/lib/not-found.ts diff --git a/src/lib/openapi.ts b/packages/starpod/src/lib/openapi.ts similarity index 100% rename from src/lib/openapi.ts rename to packages/starpod/src/lib/openapi.ts diff --git a/src/lib/optimize-episode-image.ts b/packages/starpod/src/lib/optimize-episode-image.ts similarity index 100% rename from src/lib/optimize-episode-image.ts rename to packages/starpod/src/lib/optimize-episode-image.ts diff --git a/packages/starpod/src/lib/rehype-transcript-timestamps.d.mts b/packages/starpod/src/lib/rehype-transcript-timestamps.d.mts new file mode 100644 index 00000000..ebf294af --- /dev/null +++ b/packages/starpod/src/lib/rehype-transcript-timestamps.d.mts @@ -0,0 +1,4 @@ +type RehypeTranscriptTimestamps = () => (tree: unknown) => void; + +declare const rehypeTranscriptTimestamps: RehypeTranscriptTimestamps; +export default rehypeTranscriptTimestamps; diff --git a/src/lib/rehype-transcript-timestamps.mjs b/packages/starpod/src/lib/rehype-transcript-timestamps.mjs similarity index 100% rename from src/lib/rehype-transcript-timestamps.mjs rename to packages/starpod/src/lib/rehype-transcript-timestamps.mjs diff --git a/src/lib/rss.ts b/packages/starpod/src/lib/rss.ts similarity index 84% rename from src/lib/rss.ts rename to packages/starpod/src/lib/rss.ts index e562e615..fa5a47fa 100644 --- a/src/lib/rss.ts +++ b/packages/starpod/src/lib/rss.ts @@ -11,9 +11,21 @@ import { } from 'valibot'; import { optimizeImage } from './optimize-episode-image'; +import type { StarpodConfig } from '../utils/config'; import { dasherize } from '../utils/dasherize'; import { truncate } from '../utils/truncate'; -import starpodConfig from '../../starpod.config'; + +// Inside Astro (dev, build, vitest) the config comes from the integration's +// virtual module. Standalone scripts (e.g. `db/seed.ts` under tsx) have no +// Vite, so they must pass their config explicitly and the dynamic import is +// never reached. +async function resolveRssFeed(config?: StarpodConfig): Promise { + if (config) { + return config.rssFeed; + } + const { default: starpodConfig } = await import('virtual:starpod/config'); + return starpodConfig.rssFeed; +} export interface Show { title: string; @@ -75,13 +87,13 @@ function pickTranscript( let showInfoCache: Show | null = null; -export async function getShowInfo() { +export async function getShowInfo(config?: StarpodConfig) { if (showInfoCache) { return showInfoCache; } // @ts-expect-error - const showInfo = (await parseFeed.parse(starpodConfig.rssFeed)) as Show; + const showInfo = (await parseFeed.parse(await resolveRssFeed(config))) as Show; showInfo.image = (await optimizeImage(showInfo.image, { height: 640, width: 640 @@ -93,7 +105,7 @@ export async function getShowInfo() { let episodesCache: Array | null = null; -export async function getAllEpisodes() { +export async function getAllEpisodes(config?: StarpodConfig) { if (episodesCache) { return episodesCache; } @@ -125,7 +137,7 @@ export async function getAllEpisodes() { }); // @ts-expect-error - let feed = (await parseFeed.parse(starpodConfig.rssFeed)) as Show; + let feed = (await parseFeed.parse(await resolveRssFeed(config))) as Show; let items = parse(FeedSchema, feed).items; let episodes: Array = await Promise.all( diff --git a/src/lib/standardSite.ts b/packages/starpod/src/lib/standardSite.ts similarity index 100% rename from src/lib/standardSite.ts rename to packages/starpod/src/lib/standardSite.ts diff --git a/src/lib/transcript.ts b/packages/starpod/src/lib/transcript.ts similarity index 100% rename from src/lib/transcript.ts rename to packages/starpod/src/lib/transcript.ts diff --git a/src/pages/.well-known/site.standard.publication.ts b/packages/starpod/src/pages/.well-known/site.standard.publication.ts similarity index 100% rename from src/pages/.well-known/site.standard.publication.ts rename to packages/starpod/src/pages/.well-known/site.standard.publication.ts diff --git a/src/pages/404.astro b/packages/starpod/src/pages/404.astro similarity index 100% rename from src/pages/404.astro rename to packages/starpod/src/pages/404.astro diff --git a/src/pages/[...notFound].astro b/packages/starpod/src/pages/[...notFound].astro similarity index 100% rename from src/pages/[...notFound].astro rename to packages/starpod/src/pages/[...notFound].astro diff --git a/src/pages/[episode].astro b/packages/starpod/src/pages/[episode].astro similarity index 99% rename from src/pages/[episode].astro rename to packages/starpod/src/pages/[episode].astro index c00087ea..cbb99e0f 100644 --- a/src/pages/[episode].astro +++ b/packages/starpod/src/pages/[episode].astro @@ -2,14 +2,14 @@ import { getEntry, render } from 'astro:content'; import { eq } from 'drizzle-orm'; -import { createDb } from '../../db'; +import { createDb } from '../db'; import { Episode as DbEpisode, HostOrGuest, Person, Sponsor, SponsorForEpisode -} from '../../db/schema'; +} from '../db/schema'; import { Schema } from 'astro-seo-schema'; diff --git a/src/pages/[episode].html.md.ts b/packages/starpod/src/pages/[episode].html.md.ts similarity index 96% rename from src/pages/[episode].html.md.ts rename to packages/starpod/src/pages/[episode].html.md.ts index bab5450c..0413bef4 100644 --- a/src/pages/[episode].html.md.ts +++ b/packages/starpod/src/pages/[episode].html.md.ts @@ -4,7 +4,7 @@ import type { APIRoute } from 'astro'; import { cleanTranscript, generateEpisodeMarkdown } from '../lib/llms'; import { getAllEpisodes, getShowInfo } from '../lib/rss'; import { getRssTranscriptText } from '../lib/transcript'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; export async function getStaticPaths() { const allEpisodes = await getAllEpisodes(); diff --git a/src/pages/about.astro b/packages/starpod/src/pages/about.astro similarity index 97% rename from src/pages/about.astro rename to packages/starpod/src/pages/about.astro index 729658e1..c83bf3e6 100644 --- a/src/pages/about.astro +++ b/packages/starpod/src/pages/about.astro @@ -3,7 +3,7 @@ import type { ImageMetadata } from 'astro'; import { Image } from 'astro:assets'; import Layout from '../layouts/Layout.astro'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; import LargePlatforms from '../components/LargePlatforms.astro'; const { hosts } = starpodConfig; diff --git a/src/pages/about.html.md.ts b/packages/starpod/src/pages/about.html.md.ts similarity index 88% rename from src/pages/about.html.md.ts rename to packages/starpod/src/pages/about.html.md.ts index 0d51e17d..0e9b3fb8 100644 --- a/src/pages/about.html.md.ts +++ b/packages/starpod/src/pages/about.html.md.ts @@ -2,7 +2,7 @@ import type { APIRoute } from 'astro'; import { generateAboutMarkdown } from '../lib/llms'; import { getShowInfo } from '../lib/rss'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; export const GET: APIRoute = async () => { const show = await getShowInfo(); diff --git a/src/pages/api/contact.ts b/packages/starpod/src/pages/api/contact.ts similarity index 100% rename from src/pages/api/contact.ts rename to packages/starpod/src/pages/api/contact.ts diff --git a/src/pages/api/episodes/[page].json.ts b/packages/starpod/src/pages/api/episodes/[page].json.ts similarity index 100% rename from src/pages/api/episodes/[page].json.ts rename to packages/starpod/src/pages/api/episodes/[page].json.ts diff --git a/src/pages/api/episodes/search.json.ts b/packages/starpod/src/pages/api/episodes/search.json.ts similarity index 100% rename from src/pages/api/episodes/search.json.ts rename to packages/starpod/src/pages/api/episodes/search.json.ts diff --git a/src/pages/contact.astro b/packages/starpod/src/pages/contact.astro similarity index 100% rename from src/pages/contact.astro rename to packages/starpod/src/pages/contact.astro diff --git a/src/pages/episodes-index.html.md.ts b/packages/starpod/src/pages/episodes-index.html.md.ts similarity index 100% rename from src/pages/episodes-index.html.md.ts rename to packages/starpod/src/pages/episodes-index.html.md.ts diff --git a/src/pages/for-llms.astro b/packages/starpod/src/pages/for-llms.astro similarity index 99% rename from src/pages/for-llms.astro rename to packages/starpod/src/pages/for-llms.astro index f825d6c3..09527a52 100644 --- a/src/pages/for-llms.astro +++ b/packages/starpod/src/pages/for-llms.astro @@ -2,7 +2,7 @@ import Layout from '../layouts/Layout.astro'; import { getAllEpisodes, getShowInfo } from '../lib/rss'; import { formatDate, formatDuration } from '../lib/llms'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; const show = await getShowInfo(); const episodes = await getAllEpisodes(); diff --git a/src/pages/for-llms.html.md.ts b/packages/starpod/src/pages/for-llms.html.md.ts similarity index 90% rename from src/pages/for-llms.html.md.ts rename to packages/starpod/src/pages/for-llms.html.md.ts index a9fedf28..eeef10c1 100644 --- a/src/pages/for-llms.html.md.ts +++ b/packages/starpod/src/pages/for-llms.html.md.ts @@ -2,7 +2,7 @@ import type { APIRoute } from 'astro'; import { generateForLlmsMarkdown } from '../lib/llms'; import { getAllEpisodes, getShowInfo } from '../lib/rss'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; export const GET: APIRoute = async ({ site }) => { const show = await getShowInfo(); diff --git a/src/pages/index.astro b/packages/starpod/src/pages/index.astro similarity index 100% rename from src/pages/index.astro rename to packages/starpod/src/pages/index.astro diff --git a/src/pages/llms.txt.ts b/packages/starpod/src/pages/llms.txt.ts similarity index 92% rename from src/pages/llms.txt.ts rename to packages/starpod/src/pages/llms.txt.ts index 9b6767e1..39e1125a 100644 --- a/src/pages/llms.txt.ts +++ b/packages/starpod/src/pages/llms.txt.ts @@ -2,7 +2,7 @@ import type { APIRoute } from 'astro'; import { generateLlmsTxt } from '../lib/llms'; import { getAllEpisodes, getShowInfo } from '../lib/rss'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; export const GET: APIRoute = async ({ site }) => { const show = await getShowInfo(); diff --git a/src/pages/openapi.json.ts b/packages/starpod/src/pages/openapi.json.ts similarity index 89% rename from src/pages/openapi.json.ts rename to packages/starpod/src/pages/openapi.json.ts index bf4d506c..34b1e8b4 100644 --- a/src/pages/openapi.json.ts +++ b/packages/starpod/src/pages/openapi.json.ts @@ -2,7 +2,7 @@ import type { APIRoute } from 'astro'; import { generateOpenApiSpec } from '../lib/openapi'; import { getShowInfo } from '../lib/rss'; -import starpodConfig from '../../starpod.config'; +import starpodConfig from 'virtual:starpod/config'; export const GET: APIRoute = async ({ site }) => { const show = await getShowInfo(); diff --git a/src/pages/robots.txt.ts b/packages/starpod/src/pages/robots.txt.ts similarity index 100% rename from src/pages/robots.txt.ts rename to packages/starpod/src/pages/robots.txt.ts diff --git a/src/pages/sponsor.astro b/packages/starpod/src/pages/sponsor.astro similarity index 100% rename from src/pages/sponsor.astro rename to packages/starpod/src/pages/sponsor.astro diff --git a/src/styles/buttons.css b/packages/starpod/src/styles/buttons.css similarity index 100% rename from src/styles/buttons.css rename to packages/starpod/src/styles/buttons.css diff --git a/src/styles/global.css b/packages/starpod/src/styles/global.css similarity index 100% rename from src/styles/global.css rename to packages/starpod/src/styles/global.css diff --git a/src/styles/gradient-icon.css b/packages/starpod/src/styles/gradient-icon.css similarity index 100% rename from src/styles/gradient-icon.css rename to packages/starpod/src/styles/gradient-icon.css diff --git a/src/styles/search-icon.css b/packages/starpod/src/styles/search-icon.css similarity index 100% rename from src/styles/search-icon.css rename to packages/starpod/src/styles/search-icon.css diff --git a/src/styles/tailwind.css b/packages/starpod/src/styles/tailwind.css similarity index 87% rename from src/styles/tailwind.css rename to packages/starpod/src/styles/tailwind.css index fdd85d82..6219eb38 100644 --- a/src/styles/tailwind.css +++ b/packages/starpod/src/styles/tailwind.css @@ -1,5 +1,14 @@ @import 'tailwindcss'; +/* + Tailwind's automatic source detection skips node_modules, so when this + package is installed as a dependency its own templates must be registered + explicitly. Paths are relative to this stylesheet. +*/ +@source '../components'; +@source '../layouts'; +@source '../pages'; + @plugin '@tailwindcss/forms'; @plugin '@tailwindcss/typography'; diff --git a/src/svgs/404-dark.svg b/packages/starpod/src/svgs/404-dark.svg similarity index 100% rename from src/svgs/404-dark.svg rename to packages/starpod/src/svgs/404-dark.svg diff --git a/src/svgs/404-light.svg b/packages/starpod/src/svgs/404-light.svg similarity index 100% rename from src/svgs/404-light.svg rename to packages/starpod/src/svgs/404-light.svg diff --git a/src/svgs/apple.svg b/packages/starpod/src/svgs/apple.svg similarity index 100% rename from src/svgs/apple.svg rename to packages/starpod/src/svgs/apple.svg diff --git a/src/svgs/overcast.svg b/packages/starpod/src/svgs/overcast.svg similarity index 100% rename from src/svgs/overcast.svg rename to packages/starpod/src/svgs/overcast.svg diff --git a/src/svgs/pocket-casts.svg b/packages/starpod/src/svgs/pocket-casts.svg similarity index 100% rename from src/svgs/pocket-casts.svg rename to packages/starpod/src/svgs/pocket-casts.svg diff --git a/src/svgs/spotify.svg b/packages/starpod/src/svgs/spotify.svg similarity index 100% rename from src/svgs/spotify.svg rename to packages/starpod/src/svgs/spotify.svg diff --git a/src/svgs/ufo-dark.svg b/packages/starpod/src/svgs/ufo-dark.svg similarity index 100% rename from src/svgs/ufo-dark.svg rename to packages/starpod/src/svgs/ufo-dark.svg diff --git a/src/svgs/ufo-light.svg b/packages/starpod/src/svgs/ufo-light.svg similarity index 100% rename from src/svgs/ufo-light.svg rename to packages/starpod/src/svgs/ufo-light.svg diff --git a/src/svgs/youtube.svg b/packages/starpod/src/svgs/youtube.svg similarity index 100% rename from src/svgs/youtube.svg rename to packages/starpod/src/svgs/youtube.svg diff --git a/src/utils/config.ts b/packages/starpod/src/utils/config.ts similarity index 100% rename from src/utils/config.ts rename to packages/starpod/src/utils/config.ts diff --git a/src/utils/dasherize.ts b/packages/starpod/src/utils/dasherize.ts similarity index 100% rename from src/utils/dasherize.ts rename to packages/starpod/src/utils/dasherize.ts diff --git a/src/utils/truncate.ts b/packages/starpod/src/utils/truncate.ts similarity index 100% rename from src/utils/truncate.ts rename to packages/starpod/src/utils/truncate.ts diff --git a/packages/starpod/src/vercel-md-negotiation.d.mts b/packages/starpod/src/vercel-md-negotiation.d.mts new file mode 100644 index 00000000..5cb65bef --- /dev/null +++ b/packages/starpod/src/vercel-md-negotiation.d.mts @@ -0,0 +1,16 @@ +interface VercelRoute { + [key: string]: unknown; +} + +interface VercelOutputConfig { + routes?: VercelRoute[]; + [key: string]: unknown; +} + +export function collectMarkdownPaths(staticDir: string): string[]; +export function buildNegotiationRoutes(mdPaths: string[]): VercelRoute[]; +export function patchConfig( + config: VercelOutputConfig, + mdPaths: string[] +): { config: VercelOutputConfig; inserted: number }; +export function main(outputDir?: string): void; diff --git a/scripts/vercel-md-negotiation.mjs b/packages/starpod/src/vercel-md-negotiation.mjs similarity index 100% rename from scripts/vercel-md-negotiation.mjs rename to packages/starpod/src/vercel-md-negotiation.mjs diff --git a/packages/starpod/src/virtual.d.ts b/packages/starpod/src/virtual.d.ts new file mode 100644 index 00000000..cb95a312 --- /dev/null +++ b/packages/starpod/src/virtual.d.ts @@ -0,0 +1,4 @@ +declare module 'virtual:starpod/config' { + const config: import('./utils/config').StarpodConfig; + export default config; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 389b06ca..5d1d71b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,48 +11,27 @@ importers: .: dependencies: - '@astrojs/preact': - specifier: ^5.1.4 - version: 5.1.4(@babel/core@7.29.7)(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(preact@10.29.2)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) '@astrojs/vercel': specifier: ^10.0.8 version: 10.0.8(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(react@19.0.0)(rollup@4.60.4) '@bryanguffey/astro-standard-site': specifier: ^1.0.3 version: 1.0.3(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0)) - '@libsql/client': - specifier: ^0.17.3 - version: 0.17.3 - '@preact/signals': - specifier: ^2.9.1 - version: 2.9.1(preact@10.29.2) - '@vercel/analytics': - specifier: ^1.6.1 - version: 1.6.1(react@19.0.0) - '@vercel/speed-insights': - specifier: ^1.3.1 - version: 1.3.1(react@19.0.0) astro: specifier: 6.4.2 version: 6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) - astro-seo-schema: - specifier: ^5.2.0 - version: 5.2.0(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(schema-dts@1.1.5) - atropos: - specifier: ^2.0.2 - version: 2.0.2 drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@libsql/client@0.17.3) - preact: - specifier: ^10.29.2 - version: 10.29.2 + html-to-text: + specifier: ^9.0.5 + version: 9.0.5 rss-to-json: specifier: ^2.1.1 version: 2.1.1 - schema-dts: - specifier: ^1.1.5 - version: 1.1.5 + starpod: + specifier: workspace:* + version: link:packages/starpod valibot: specifier: ^1.4.1 version: 1.4.1(typescript@5.9.3) @@ -60,24 +39,12 @@ importers: '@astrojs/check': specifier: ^0.9.9 version: 0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.9.3) - '@astrojs/sitemap': - specifier: ^3.7.3 - version: 3.7.3 '@eslint/js': specifier: ^9.39.4 version: 9.39.4 '@playwright/test': specifier: ^1.60.0 version: 1.60.0 - '@tailwindcss/forms': - specifier: ^0.5.11 - version: 0.5.11(tailwindcss@4.3.0) - '@tailwindcss/typography': - specifier: ^0.5.19 - version: 0.5.19(tailwindcss@4.3.0) - '@tailwindcss/vite': - specifier: ^4.3.0 - version: 4.3.0(vite@7.3.3(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -111,12 +78,12 @@ importers: globals: specifier: ^16.5.0 version: 16.5.0 - html-to-text: - specifier: ^9.0.5 - version: 9.0.5 jsdom: specifier: ^27.4.0 version: 27.4.0 + preact: + specifier: ^10.29.2 + version: 10.29.2 prettier: specifier: ^3.8.3 version: 3.8.3 @@ -145,6 +112,73 @@ importers: specifier: ^4.1.7 version: 4.1.7(@types/node@24.12.4)(jsdom@27.4.0)(vite@7.3.3(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/starpod: + dependencies: + '@astrojs/preact': + specifier: ^5.1.4 + version: 5.1.4(@babel/core@7.29.7)(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(preact@10.29.2)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) + '@astrojs/sitemap': + specifier: ^3.7.3 + version: 3.7.3 + '@bryanguffey/astro-standard-site': + specifier: ^1.0.3 + version: 1.0.3(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0)) + '@libsql/client': + specifier: ^0.17.3 + version: 0.17.3 + '@preact/signals': + specifier: ^2.9.1 + version: 2.9.1(preact@10.29.2) + '@tailwindcss/forms': + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@4.3.0) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.3.0) + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@7.3.3(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vercel/speed-insights': + specifier: ^1.3.1 + version: 1.3.1(react@19.0.0) + astro-seo-schema: + specifier: ^5.2.0 + version: 5.2.0(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(schema-dts@1.1.5) + atropos: + specifier: ^2.0.2 + version: 2.0.2 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@libsql/client@0.17.3) + html-to-text: + specifier: ^9.0.5 + version: 9.0.5 + preact: + specifier: ^10.29.2 + version: 10.29.2 + rss-to-json: + specifier: ^2.1.1 + version: 2.1.1 + schema-dts: + specifier: ^1.1.5 + version: 1.1.5 + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + valibot: + specifier: ^1.4.1 + version: 1.4.1(typescript@5.9.3) + devDependencies: + '@types/html-to-text': + specifier: ^9.0.4 + version: 9.0.4 + astro: + specifier: 6.4.2 + version: 6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages: '@acemir/cssom@0.9.31': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..924b55f4 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/scripts/publish-episodes.ts b/scripts/publish-episodes.ts index e2a11263..63827bd5 100644 --- a/scripts/publish-episodes.ts +++ b/scripts/publish-episodes.ts @@ -26,7 +26,7 @@ import { } from '@bryanguffey/astro-standard-site'; import starpodConfig from '../starpod.config'; -import { dasherize } from '../src/utils/dasherize'; +import { dasherize } from 'starpod/src/utils/dasherize'; const BACKFILL = process.argv.includes('--backfill'); // Set by the GitHub workflow when it has just triggered a site rebuild: diff --git a/src/content.config.ts b/src/content.config.ts index 6120d914..0e0c7d44 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -1,10 +1,10 @@ import { defineCollection } from 'astro:content'; -import { glob } from 'astro/loaders'; +import { transcriptsLoader } from 'starpod/content'; const transcripts = defineCollection({ - loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/transcripts' }), + loader: transcriptsLoader() }); export const collections = { transcripts -}; \ No newline at end of file +}; diff --git a/src/content/config.ts b/src/content/config.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/starpod.config.ts b/starpod.config.ts index 5a38d9ea..93291b76 100644 --- a/starpod.config.ts +++ b/starpod.config.ts @@ -1,4 +1,4 @@ -import { defineStarpodConfig } from 'src/utils/config'; +import { defineStarpodConfig } from 'starpod/config'; export default defineStarpodConfig({ blurb: diff --git a/tests/unit/ForwardButton.test.tsx b/tests/unit/ForwardButton.test.tsx index 17591c1e..0a8e7420 100644 --- a/tests/unit/ForwardButton.test.tsx +++ b/tests/unit/ForwardButton.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import ForwardButton from '../../src/components/player/ForwardButton'; +import ForwardButton from 'starpod/src/components/player/ForwardButton'; import { createRef } from 'preact'; // Mock audio element diff --git a/tests/unit/FullPlayButton.test.tsx b/tests/unit/FullPlayButton.test.tsx index d4df6799..30b0e746 100644 --- a/tests/unit/FullPlayButton.test.tsx +++ b/tests/unit/FullPlayButton.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import FullPlayButton from '../../src/components/FullPlayButton'; -import { currentEpisode, isPlaying } from '../../src/components/state'; +import FullPlayButton from 'starpod/src/components/FullPlayButton'; +import { currentEpisode, isPlaying } from 'starpod/src/components/state'; const mockEpisode = { id: 'test-episode-1', diff --git a/tests/unit/MuteButton.test.tsx b/tests/unit/MuteButton.test.tsx index 6453fa4a..c458bd27 100644 --- a/tests/unit/MuteButton.test.tsx +++ b/tests/unit/MuteButton.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import MuteButton from '../../src/components/player/MuteButton'; -import { isMuted } from '../../src/components/state'; +import MuteButton from 'starpod/src/components/player/MuteButton'; +import { isMuted } from 'starpod/src/components/state'; describe('MuteButton', () => { beforeEach(() => { diff --git a/tests/unit/PlayButton.test.tsx b/tests/unit/PlayButton.test.tsx index 81d518ad..f736f2b7 100644 --- a/tests/unit/PlayButton.test.tsx +++ b/tests/unit/PlayButton.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import PlayButton from '../../src/components/player/PlayButton'; -import { currentEpisode, isPlaying } from '../../src/components/state'; +import PlayButton from 'starpod/src/components/player/PlayButton'; +import { currentEpisode, isPlaying } from 'starpod/src/components/state'; const mockEpisode = { id: 'test-episode-1', diff --git a/tests/unit/PlaybackRateButton.test.tsx b/tests/unit/PlaybackRateButton.test.tsx index bc23e7c3..c54cda50 100644 --- a/tests/unit/PlaybackRateButton.test.tsx +++ b/tests/unit/PlaybackRateButton.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import PlaybackRateButton from '../../src/components/player/PlaybackRateButton'; +import PlaybackRateButton from 'starpod/src/components/player/PlaybackRateButton'; import { createRef } from 'preact'; // Mock audio element diff --git a/tests/unit/Player.test.tsx b/tests/unit/Player.test.tsx index 9b822119..17f5fe23 100644 --- a/tests/unit/Player.test.tsx +++ b/tests/unit/Player.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/preact'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import Player from '../../src/components/Player'; -import { currentEpisode, isPlaying, isMuted } from '../../src/components/state'; +import Player from 'starpod/src/components/Player'; +import { currentEpisode, isPlaying, isMuted } from 'starpod/src/components/state'; const mockEpisode = { id: 'test-episode-1', diff --git a/tests/unit/RewindButton.test.tsx b/tests/unit/RewindButton.test.tsx index 8d9ec09d..75559327 100644 --- a/tests/unit/RewindButton.test.tsx +++ b/tests/unit/RewindButton.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import RewindButton from '../../src/components/player/RewindButton'; +import RewindButton from 'starpod/src/components/player/RewindButton'; import { createRef } from 'preact'; // Mock audio element diff --git a/tests/unit/SearchButton.test.tsx b/tests/unit/SearchButton.test.tsx index 8bb6e2e4..1b65e44c 100644 --- a/tests/unit/SearchButton.test.tsx +++ b/tests/unit/SearchButton.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import SearchButton from '../../src/components/SearchButton'; -import { isSearchOpen } from '../../src/components/state'; +import SearchButton from 'starpod/src/components/SearchButton'; +import { isSearchOpen } from 'starpod/src/components/state'; describe('SearchButton', () => { beforeEach(() => { diff --git a/tests/unit/SearchDialog.test.tsx b/tests/unit/SearchDialog.test.tsx index fd4a7950..d3dec7a7 100644 --- a/tests/unit/SearchDialog.test.tsx +++ b/tests/unit/SearchDialog.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest'; -import SearchDialog from '../../src/components/SearchDialog'; -import { isSearchOpen } from '../../src/components/state'; +import SearchDialog from 'starpod/src/components/SearchDialog'; +import { isSearchOpen } from 'starpod/src/components/state'; // Mock fetch for episodes const mockEpisodes = [ diff --git a/tests/unit/Slider.test.tsx b/tests/unit/Slider.test.tsx index 857c8530..140f7614 100644 --- a/tests/unit/Slider.test.tsx +++ b/tests/unit/Slider.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/preact'; import { beforeEach, describe, expect, it } from 'vitest'; -import Slider from '../../src/components/player/Slider'; +import Slider from 'starpod/src/components/player/Slider'; import { createRef } from 'preact'; // Mock audio element diff --git a/tests/unit/contact-api.test.ts b/tests/unit/contact-api.test.ts index 46879ecd..5da92711 100644 --- a/tests/unit/contact-api.test.ts +++ b/tests/unit/contact-api.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { ALL, POST } from '../../src/pages/api/contact'; +import { ALL, POST } from 'starpod/src/pages/api/contact'; type ApiContext = Parameters[0]; diff --git a/tests/unit/llms.test.ts b/tests/unit/llms.test.ts index fc316247..c7300150 100644 --- a/tests/unit/llms.test.ts +++ b/tests/unit/llms.test.ts @@ -10,9 +10,9 @@ import { generateEpisodesIndex, generateForLlmsMarkdown, generateAboutMarkdown -} from '../../src/lib/llms'; -import type { Episode, Show } from '../../src/lib/rss'; -import type { StarpodConfig } from '../../src/utils/config'; +} from 'starpod/src/lib/llms'; +import type { Episode, Show } from 'starpod/src/lib/rss'; +import type { StarpodConfig } from 'starpod/src/utils/config'; describe('LLM Utilities', () => { describe('formatDuration', () => { diff --git a/tests/unit/not-found.test.ts b/tests/unit/not-found.test.ts index 0655256e..e29746ec 100644 --- a/tests/unit/not-found.test.ts +++ b/tests/unit/not-found.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { generateNotFoundMarkdown } from '../../src/lib/not-found'; -import type { Show } from '../../src/lib/rss'; +import { generateNotFoundMarkdown } from 'starpod/src/lib/not-found'; +import type { Show } from 'starpod/src/lib/rss'; describe('generateNotFoundMarkdown', () => { const mockShow: Show = { diff --git a/tests/unit/openapi.test.ts b/tests/unit/openapi.test.ts index 2a498e06..fd7aed14 100644 --- a/tests/unit/openapi.test.ts +++ b/tests/unit/openapi.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { generateOpenApiSpec } from '../../src/lib/openapi'; -import type { Show } from '../../src/lib/rss'; -import type { StarpodConfig } from '../../src/utils/config'; +import { generateOpenApiSpec } from 'starpod/src/lib/openapi'; +import type { Show } from 'starpod/src/lib/rss'; +import type { StarpodConfig } from 'starpod/src/utils/config'; const mockShow: Show = { title: 'Test Podcast', diff --git a/tests/unit/rehype-transcript-timestamps.test.ts b/tests/unit/rehype-transcript-timestamps.test.ts index edce73ab..8bb9d13a 100644 --- a/tests/unit/rehype-transcript-timestamps.test.ts +++ b/tests/unit/rehype-transcript-timestamps.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import rehypeTranscriptTimestamps from '../../src/lib/rehype-transcript-timestamps.mjs'; +import rehypeTranscriptTimestamps from 'starpod/src/lib/rehype-transcript-timestamps.mjs'; type HastNode = { type: string; diff --git a/tests/unit/robots.test.ts b/tests/unit/robots.test.ts index 9ab1d79d..98ac4aa2 100644 --- a/tests/unit/robots.test.ts +++ b/tests/unit/robots.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, beforeEach } from 'vitest'; -import { getRobotsTxt } from '../../src/pages/robots.txt'; +import { getRobotsTxt } from 'starpod/src/pages/robots.txt'; describe('Robots.txt', () => { let mockSite: URL; diff --git a/tests/unit/transcript.test.ts b/tests/unit/transcript.test.ts index 01bee066..413f767d 100644 --- a/tests/unit/transcript.test.ts +++ b/tests/unit/transcript.test.ts @@ -3,8 +3,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { getRssTranscriptParagraphs, getRssTranscriptText -} from '../../src/lib/transcript'; -import type { Episode } from '../../src/lib/rss'; +} from 'starpod/src/lib/transcript'; +import type { Episode } from 'starpod/src/lib/rss'; // Each test uses a unique transcript URL because the module caches parsed // transcripts by URL — reusing a URL would return a previous test's cached diff --git a/tests/unit/vercel-md-negotiation.test.ts b/tests/unit/vercel-md-negotiation.test.ts index b1d7bd51..873ed490 100644 --- a/tests/unit/vercel-md-negotiation.test.ts +++ b/tests/unit/vercel-md-negotiation.test.ts @@ -7,7 +7,7 @@ import { buildNegotiationRoutes, collectMarkdownPaths, patchConfig -} from '../../scripts/vercel-md-negotiation.mjs'; +} from 'starpod/src/vercel-md-negotiation.mjs'; const ACCEPT_MD = [{ type: 'header', key: 'accept', value: '.*text/markdown.*' }]; diff --git a/vitest.config.ts b/vitest.config.ts index 91f56210..73583223 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,5 @@ +import { fileURLToPath } from 'node:url'; + import { defineConfig } from 'vitest/config'; export default defineConfig({ @@ -10,7 +12,12 @@ export default defineConfig({ }, resolve: { alias: { - '@': '/src' + '@': '/src', + // Outside Astro there is no integration to provide the virtual config + // module, so tests resolve it straight to the site's config file. + 'virtual:starpod/config': fileURLToPath( + new URL('./starpod.config.ts', import.meta.url) + ) } } }); From 3db0141225be37b83fcbec531ea07c098318002f Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 11:07:19 -0400 Subject: [PATCH 02/12] Fix type resolution, hook ordering, and renderer resolution in the integration spike - tsconfig paths + vitest alias so starpod/src/* imports resolve for TS and tests (TS does not resolve the exports wildcard the way Vite does) - Vercel markdown negotiation moves to a process exit handler: the adapter's astro:build:done runs after the integration's and rewrites config.json - @astrojs/preact becomes a direct site dependency: Astro resolves the renderer client entrypoint from the project root, so the consumer must depend on it (matches how framework renderers are normally installed) - Tighten the vercel-md-negotiation type declarations to match its tests Co-Authored-By: Claude Fable 5 --- package.json | 1 + packages/starpod/src/index.ts | 13 +++++++++---- packages/starpod/src/vercel-md-negotiation.d.mts | 16 +++++++++++++--- pnpm-lock.yaml | 4 ++++ tsconfig.json | 3 +++ vitest.config.ts | 5 +++++ 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 0b451fff..b7dee521 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test:unit": "vitest" }, "dependencies": { + "@astrojs/preact": "^5.1.4", "@astrojs/vercel": "^10.0.8", "@bryanguffey/astro-standard-site": "^1.0.3", "astro": "6.4.2", diff --git a/packages/starpod/src/index.ts b/packages/starpod/src/index.ts index d446662d..72301ade 100644 --- a/packages/starpod/src/index.ts +++ b/packages/starpod/src/index.ts @@ -136,10 +136,15 @@ export default function starpod(starpodConfig: StarpodConfig): AstroIntegration }, 'astro:build:done': () => { // Injects `Accept: text/markdown` content-negotiation routes into the - // Vercel build output. No-ops for other adapters. - applyVercelMarkdownNegotiation( - fileURLToPath(new URL('./.vercel/output', projectRoot)) - ); + // Vercel build output. No-ops for other adapters. The adapter's own + // astro:build:done hook runs after this one and writes + // `.vercel/output/config.json`, so the patch must wait until process + // exit (the patch is synchronous fs work, which is safe there). + process.once('exit', () => { + applyVercelMarkdownNegotiation( + fileURLToPath(new URL('./.vercel/output', projectRoot)) + ); + }); } } }; diff --git a/packages/starpod/src/vercel-md-negotiation.d.mts b/packages/starpod/src/vercel-md-negotiation.d.mts index 5cb65bef..0e35565b 100644 --- a/packages/starpod/src/vercel-md-negotiation.d.mts +++ b/packages/starpod/src/vercel-md-negotiation.d.mts @@ -1,14 +1,24 @@ interface VercelRoute { - [key: string]: unknown; + src?: string; + dest?: string; + headers?: Record; + status?: number; + handle?: string; + has?: Array<{ type: string; key: string; value: string }>; + continue?: boolean; +} + +interface NegotiationRoute extends VercelRoute { + src: string; } interface VercelOutputConfig { - routes?: VercelRoute[]; + routes: VercelRoute[]; [key: string]: unknown; } export function collectMarkdownPaths(staticDir: string): string[]; -export function buildNegotiationRoutes(mdPaths: string[]): VercelRoute[]; +export function buildNegotiationRoutes(mdPaths: string[]): NegotiationRoute[]; export function patchConfig( config: VercelOutputConfig, mdPaths: string[] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d1d71b1..c4e269be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: .: dependencies: + '@astrojs/preact': + specifier: ^5.1.4 + version: 5.1.4(@babel/core@7.29.7)(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(preact@10.29.2)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) '@astrojs/vercel': specifier: ^10.0.8 version: 10.0.8(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(react@19.0.0)(rollup@4.60.4) @@ -2542,6 +2545,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' diff --git a/tsconfig.json b/tsconfig.json index e3696fbb..d6e44e15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,9 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "baseUrl": ".", + "paths": { + "starpod/src/*": ["./packages/starpod/src/*"] + }, "esModuleInterop": true, "strictNullChecks": true, "verbatimModuleSyntax": true, diff --git a/vitest.config.ts b/vitest.config.ts index 73583223..e698fa55 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,6 +13,11 @@ export default defineConfig({ resolve: { alias: { '@': '/src', + // Resolve package-internal imports as filesystem paths so + // directory-index imports (e.g. .../player/Slider) work in tests. + 'starpod/src': fileURLToPath( + new URL('./packages/starpod/src', import.meta.url) + ), // Outside Astro there is no integration to provide the virtual config // module, so tests resolve it straight to the site's config file. 'virtual:starpod/config': fileURLToPath( From 4708dd6b2552890e00125c63a8e1915cc90bd631 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 21:26:42 -0400 Subject: [PATCH 03/12] Production-ready integration: hook-time negotiation, opt-in DB, overrides, bundled assets - Vercel markdown negotiation now runs inside astro:build:done using the hook's own assets map (the adapter writes config.json before user hooks but copies static files after them, so the filesystem can't be scanned) - @astrojs/preact and preact become peerDependencies; the package ships a tsconfig.json (nearest-tsconfig JSX settings for installed copies) and widens the preset's JSX exclude so starpod's own .tsx transforms when it lives in node_modules - starpod(config, options): options.database gates the Drizzle/Turso guests+sponsors layer (default off; sections simply don't render), options.components overrides built-ins via virtual modules, and options.customCss loads site stylesheets after the built-in ones - Engine assets (player icons, platform logos, dots, avatars) move into the package and are bundled by Vite instead of expected in consumer public/; host/guest/sponsor image lookups fall back gracefully when a site has not provided photos yet - Normalize rss-to-json's dual CJS interop shape (externalized vs bundled) Co-Authored-By: Claude Fable 5 --- astro.config.mjs | 2 +- packages/starpod/.impeccable/hook.cache.json | 1 + packages/starpod/package.json | 9 +- .../starpod/src/assets}/apple.svg | 0 .../starpod/src/assets}/avatar-dark.png | Bin .../starpod/src/assets}/avatar-light.png | Bin .../starpod/src/assets}/dots-dark.svg | 0 .../starpod/src/assets}/dots-light.svg | 0 .../starpod/src/assets}/forward-icon.svg | 0 .../starpod/src/assets}/mute-icon.svg | 0 .../starpod/src/assets}/overcast.svg | 0 .../starpod/src/assets}/pocket-casts.svg | 0 .../starpod/src/assets}/rewind-icon.svg | 0 .../starpod/src/assets}/rocket-dark.svg | 0 .../starpod/src/assets}/rocket-light.svg | 0 .../starpod/src/assets}/search-icon.svg | 0 .../starpod/src/assets}/spotify.svg | 0 .../starpod/src/assets}/unmute-icon.svg | 0 .../starpod/src/assets}/youtube.svg | 0 packages/starpod/src/components/Hosts.astro | 9 +- .../starpod/src/components/Platforms.astro | 10 +- .../episode/CreatorsAndGuests.astro | 9 +- .../src/components/episode/Sponsors.astro | 4 +- .../player/ForwardButton/styles.css | 2 +- .../components/player/MuteButton/styles.css | 4 +- .../components/player/RewindButton/styles.css | 2 +- .../src/components/player/Slider/styles.css | 8 +- packages/starpod/src/index.ts | 138 +++++++++++++++--- packages/starpod/src/layouts/Layout.astro | 13 +- packages/starpod/src/lib/rss.ts | 17 ++- packages/starpod/src/pages/404.astro | 2 +- .../starpod/src/pages/[...notFound].astro | 2 +- packages/starpod/src/pages/[episode].astro | 72 +++++---- packages/starpod/src/pages/about.astro | 11 +- packages/starpod/src/pages/index.astro | 2 +- packages/starpod/src/pages/sponsor.astro | 5 +- packages/starpod/src/styles/search-icon.css | 2 +- packages/starpod/src/styles/tailwind.css | 4 +- .../starpod/src/vercel-md-negotiation.d.mts | 3 +- .../starpod/src/vercel-md-negotiation.mjs | 22 ++- packages/starpod/src/virtual.d.ts | 12 ++ packages/starpod/tsconfig.json | 12 ++ pnpm-lock.yaml | 12 +- tsconfig.json | 2 +- 44 files changed, 290 insertions(+), 101 deletions(-) create mode 100644 packages/starpod/.impeccable/hook.cache.json rename {public/images => packages/starpod/src/assets}/apple.svg (100%) rename {src/img/people => packages/starpod/src/assets}/avatar-dark.png (100%) rename {src/img/people => packages/starpod/src/assets}/avatar-light.png (100%) rename {public/images => packages/starpod/src/assets}/dots-dark.svg (100%) rename {public/images => packages/starpod/src/assets}/dots-light.svg (100%) rename {public/images => packages/starpod/src/assets}/forward-icon.svg (100%) rename {public/images => packages/starpod/src/assets}/mute-icon.svg (100%) rename {public/images => packages/starpod/src/assets}/overcast.svg (100%) rename {public/images => packages/starpod/src/assets}/pocket-casts.svg (100%) rename {public/images => packages/starpod/src/assets}/rewind-icon.svg (100%) rename {public/images => packages/starpod/src/assets}/rocket-dark.svg (100%) rename {public/images => packages/starpod/src/assets}/rocket-light.svg (100%) rename {public/images => packages/starpod/src/assets}/search-icon.svg (100%) rename {public/images => packages/starpod/src/assets}/spotify.svg (100%) rename {public/images => packages/starpod/src/assets}/unmute-icon.svg (100%) rename {public/images => packages/starpod/src/assets}/youtube.svg (100%) create mode 100644 packages/starpod/tsconfig.json diff --git a/astro.config.mjs b/astro.config.mjs index 90b22e00..9ffe8404 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -27,7 +27,7 @@ export default defineConfig({ } }), site: 'https://whiskey.fm', - integrations: [starpod(starpodConfig)], + integrations: [starpod(starpodConfig, { database: true })], // These were specific redirects we needed for our podcast, if you do not have any routes to redirect, you can safely remove this. redirects: { '/hot-takes-tan-stack-and-open-source-with-tanner-linsley': diff --git a/packages/starpod/.impeccable/hook.cache.json b/packages/starpod/.impeccable/hook.cache.json new file mode 100644 index 00000000..a7383d1c --- /dev/null +++ b/packages/starpod/.impeccable/hook.cache.json @@ -0,0 +1 @@ +{"version":1,"sessions":{"30f0a51d-cb66-4ef6-983c-59c87c60bcdc":{"updatedAt":1787620034782,"files":{"/Users/rwwagner90/shipshape/starpod/.claude/worktrees/astro-integration/packages/starpod/src/pages/[episode].astro":{"editCount":3,"findings":["gray-on-color:239"]},"/Users/rwwagner90/shipshape/starpod/.claude/worktrees/astro-integration/packages/starpod/src/components/episode/CreatorsAndGuests.astro":{"editCount":3,"findings":[],"cleanAcked":true}},"footerShown":true}}} \ No newline at end of file diff --git a/packages/starpod/package.json b/packages/starpod/package.json index 61d2188c..43b344cc 100644 --- a/packages/starpod/package.json +++ b/packages/starpod/package.json @@ -15,6 +15,7 @@ "withastro", "podcast" ], + "files": ["src", "tsconfig.json"], "exports": { ".": "./src/index.ts", "./config": "./src/config.ts", @@ -24,7 +25,6 @@ "./src/*": "./src/*" }, "dependencies": { - "@astrojs/preact": "^5.1.4", "@astrojs/sitemap": "^3.7.3", "@bryanguffey/astro-standard-site": "^1.0.3", "@libsql/client": "^0.17.3", @@ -37,18 +37,21 @@ "atropos": "^2.0.2", "drizzle-orm": "^0.45.2", "html-to-text": "^9.0.5", - "preact": "^10.29.2", "rss-to-json": "^2.1.1", "schema-dts": "^1.1.5", "tailwindcss": "^4.3.0", "valibot": "^1.4.1" }, "peerDependencies": { - "astro": "^6.0.0" + "@astrojs/preact": "^5.0.0", + "astro": "^6.0.0", + "preact": "^10.0.0" }, "devDependencies": { + "@astrojs/preact": "^5.1.4", "@types/html-to-text": "^9.0.4", "astro": "6.4.2", + "preact": "^10.29.2", "typescript": "^5.9.3" }, "engines": { diff --git a/public/images/apple.svg b/packages/starpod/src/assets/apple.svg similarity index 100% rename from public/images/apple.svg rename to packages/starpod/src/assets/apple.svg diff --git a/src/img/people/avatar-dark.png b/packages/starpod/src/assets/avatar-dark.png similarity index 100% rename from src/img/people/avatar-dark.png rename to packages/starpod/src/assets/avatar-dark.png diff --git a/src/img/people/avatar-light.png b/packages/starpod/src/assets/avatar-light.png similarity index 100% rename from src/img/people/avatar-light.png rename to packages/starpod/src/assets/avatar-light.png diff --git a/public/images/dots-dark.svg b/packages/starpod/src/assets/dots-dark.svg similarity index 100% rename from public/images/dots-dark.svg rename to packages/starpod/src/assets/dots-dark.svg diff --git a/public/images/dots-light.svg b/packages/starpod/src/assets/dots-light.svg similarity index 100% rename from public/images/dots-light.svg rename to packages/starpod/src/assets/dots-light.svg diff --git a/public/images/forward-icon.svg b/packages/starpod/src/assets/forward-icon.svg similarity index 100% rename from public/images/forward-icon.svg rename to packages/starpod/src/assets/forward-icon.svg diff --git a/public/images/mute-icon.svg b/packages/starpod/src/assets/mute-icon.svg similarity index 100% rename from public/images/mute-icon.svg rename to packages/starpod/src/assets/mute-icon.svg diff --git a/public/images/overcast.svg b/packages/starpod/src/assets/overcast.svg similarity index 100% rename from public/images/overcast.svg rename to packages/starpod/src/assets/overcast.svg diff --git a/public/images/pocket-casts.svg b/packages/starpod/src/assets/pocket-casts.svg similarity index 100% rename from public/images/pocket-casts.svg rename to packages/starpod/src/assets/pocket-casts.svg diff --git a/public/images/rewind-icon.svg b/packages/starpod/src/assets/rewind-icon.svg similarity index 100% rename from public/images/rewind-icon.svg rename to packages/starpod/src/assets/rewind-icon.svg diff --git a/public/images/rocket-dark.svg b/packages/starpod/src/assets/rocket-dark.svg similarity index 100% rename from public/images/rocket-dark.svg rename to packages/starpod/src/assets/rocket-dark.svg diff --git a/public/images/rocket-light.svg b/packages/starpod/src/assets/rocket-light.svg similarity index 100% rename from public/images/rocket-light.svg rename to packages/starpod/src/assets/rocket-light.svg diff --git a/public/images/search-icon.svg b/packages/starpod/src/assets/search-icon.svg similarity index 100% rename from public/images/search-icon.svg rename to packages/starpod/src/assets/search-icon.svg diff --git a/public/images/spotify.svg b/packages/starpod/src/assets/spotify.svg similarity index 100% rename from public/images/spotify.svg rename to packages/starpod/src/assets/spotify.svg diff --git a/public/images/unmute-icon.svg b/packages/starpod/src/assets/unmute-icon.svg similarity index 100% rename from public/images/unmute-icon.svg rename to packages/starpod/src/assets/unmute-icon.svg diff --git a/public/images/youtube.svg b/packages/starpod/src/assets/youtube.svg similarity index 100% rename from public/images/youtube.svg rename to packages/starpod/src/assets/youtube.svg diff --git a/packages/starpod/src/components/Hosts.astro b/packages/starpod/src/components/Hosts.astro index 42b47a5d..fe64fce2 100644 --- a/packages/starpod/src/components/Hosts.astro +++ b/packages/starpod/src/components/Hosts.astro @@ -4,10 +4,17 @@ import { Image } from 'astro:assets'; import starpodConfig from 'virtual:starpod/config'; +import avatarLight from '../assets/avatar-light.png'; + const { hosts } = starpodConfig; const images = import.meta.glob<{ default: ImageMetadata }>( '/src/img/people/*.{jpeg,jpg,png}' ); + +// Fall back to the packaged avatar when the site has not provided a photo in +// src/img/people yet. +const hostImage = (img: string) => + images[`/src/img/people/${img}`]?.() ?? avatarLight; ---
( .apple { - mask-image: url(/images/apple.svg); + mask-image: url('../assets/apple.svg'); } .overcast { - mask-image: url(/images/overcast.svg); + mask-image: url('../assets/overcast.svg'); } .pocket-casts { - mask-image: url(/images/pocket-casts.svg); + mask-image: url('../assets/pocket-casts.svg'); } .spotify { - mask-image: url(/images/spotify.svg); + mask-image: url('../assets/spotify.svg'); } .youtube { - mask-image: url(/images/youtube.svg); + mask-image: url('../assets/youtube.svg'); } diff --git a/packages/starpod/src/components/episode/CreatorsAndGuests.astro b/packages/starpod/src/components/episode/CreatorsAndGuests.astro index 358b6009..277f0574 100644 --- a/packages/starpod/src/components/episode/CreatorsAndGuests.astro +++ b/packages/starpod/src/components/episode/CreatorsAndGuests.astro @@ -2,6 +2,9 @@ import type { ImageMetadata } from 'astro'; import { Image } from 'astro:assets'; +import avatarDark from '../../assets/avatar-dark.png'; +import avatarLight from '../../assets/avatar-light.png'; + export interface Props { hostsAndGuests: Array<{ id: string; @@ -53,7 +56,7 @@ const priorityNames = ['RobbieTheWagner', 'Charles William Carpenter III', 'Adam }).map((person) => { return (
- {person.img ? + {person.img && images[`/src/img/people/${person.img}`] ? } diff --git a/packages/starpod/src/components/episode/Sponsors.astro b/packages/starpod/src/components/episode/Sponsors.astro index 66f3a402..7a804167 100644 --- a/packages/starpod/src/components/episode/Sponsors.astro +++ b/packages/starpod/src/components/episode/Sponsors.astro @@ -24,7 +24,9 @@ const svgs = import.meta.glob('/src/img/sponsors/*.svg', {
- +
{sponsor.name}
diff --git a/packages/starpod/src/components/player/ForwardButton/styles.css b/packages/starpod/src/components/player/ForwardButton/styles.css index e220fe05..e823e2d9 100644 --- a/packages/starpod/src/components/player/ForwardButton/styles.css +++ b/packages/starpod/src/components/player/ForwardButton/styles.css @@ -1,3 +1,3 @@ .forward-button { - mask-image: url(/images/forward-icon.svg); + mask-image: url('../../../assets/forward-icon.svg'); } diff --git a/packages/starpod/src/components/player/MuteButton/styles.css b/packages/starpod/src/components/player/MuteButton/styles.css index c78a6361..9c85f9ac 100644 --- a/packages/starpod/src/components/player/MuteButton/styles.css +++ b/packages/starpod/src/components/player/MuteButton/styles.css @@ -1,7 +1,7 @@ .mute-button { - mask-image: url(/images/mute-icon.svg); + mask-image: url('../../../assets/mute-icon.svg'); } .unmute-button { - mask-image: url(/images/unmute-icon.svg); + mask-image: url('../../../assets/unmute-icon.svg'); } diff --git a/packages/starpod/src/components/player/RewindButton/styles.css b/packages/starpod/src/components/player/RewindButton/styles.css index c3794419..a795fa42 100644 --- a/packages/starpod/src/components/player/RewindButton/styles.css +++ b/packages/starpod/src/components/player/RewindButton/styles.css @@ -1,3 +1,3 @@ .rewind-button { - mask-image: url(/images/rewind-icon.svg); + mask-image: url('../../../assets/rewind-icon.svg'); } diff --git a/packages/starpod/src/components/player/Slider/styles.css b/packages/starpod/src/components/player/Slider/styles.css index 790defe9..33ec5caa 100644 --- a/packages/starpod/src/components/player/Slider/styles.css +++ b/packages/starpod/src/components/player/Slider/styles.css @@ -17,7 +17,7 @@ } &::-webkit-slider-thumb { @apply relative -mt-[5px] box-content h-4 w-4 cursor-pointer appearance-none bg-contain bg-no-repeat transition-opacity duration-300 lg:opacity-0; - background-image: url('/images/rocket-dark.svg'); + background-image: url('../../../assets/rocket-dark.svg'); background-position: center center; box-shadow: none; background-color: #0000; @@ -33,16 +33,16 @@ } &::-moz-range-thumb { @apply h-4 w-4 cursor-pointer appearance-none border-0 bg-transparent bg-contain bg-no-repeat transition-opacity duration-300 outline-none lg:opacity-0; - background-image: url('/images/rocket-dark.svg'); + background-image: url('../../../assets/rocket-dark.svg'); background-position: center center; } @media (prefers-color-scheme: light) { &::-webkit-slider-thumb { - background-image: url('/images/rocket-light.svg'); + background-image: url('../../../assets/rocket-light.svg'); } &::-moz-range-thumb { - background-image: url('/images/rocket-light.svg'); + background-image: url('../../../assets/rocket-light.svg'); } } diff --git a/packages/starpod/src/index.ts b/packages/starpod/src/index.ts index 72301ade..cdf7f7c0 100644 --- a/packages/starpod/src/index.ts +++ b/packages/starpod/src/index.ts @@ -1,3 +1,4 @@ +import { resolve as resolvePath } from 'node:path'; import { fileURLToPath } from 'node:url'; import preact from '@astrojs/preact'; @@ -9,26 +10,109 @@ import type { Plugin as VitePlugin } from 'vite'; import rehypeTranscriptTimestamps from './lib/rehype-transcript-timestamps.mjs'; import { type StarpodConfig } from './utils/config'; -import { main as applyVercelMarkdownNegotiation } from './vercel-md-negotiation.mjs'; +import { + main as applyVercelMarkdownNegotiation, + pagesToMarkdownPaths +} from './vercel-md-negotiation.mjs'; + +/** + * Built-in components a site may replace via `options.components`. Each maps + * to the default implementation shipped with the package; call sites import + * them through `virtual:starpod/components/` so an override swaps the + * module everywhere at once. + */ +const OVERRIDABLE_COMPONENTS = { + Dots: './components/Dots.astro', + EpisodeList: './components/EpisodeList.astro', + Hosts: './components/Hosts.astro', + InfoCard: './components/InfoCard.astro', + LargePlatforms: './components/LargePlatforms.astro', + NotFoundContent: './components/NotFoundContent.astro', + Platforms: './components/Platforms.astro', + ShowArtwork: './components/ShowArtwork.astro' +} as const; + +export type OverridableComponent = keyof typeof OVERRIDABLE_COMPONENTS; + +export interface StarpodOptions { + /** + * Enable the Turso/Drizzle database for per-episode guests and sponsors. + * Requires `ASTRO_DB_REMOTE_URL` and `ASTRO_DB_APP_TOKEN`. When disabled + * (the default), episode pages simply render without the guests and + * sponsors sections. + */ + database?: boolean; + /** + * Replace built-in components with your own, e.g. + * `{ Hosts: './src/components/MyHosts.astro' }`. Paths are resolved from + * the project root. + */ + components?: Partial>; + /** + * Extra stylesheets loaded after the built-in styles, e.g. + * `['./src/styles/theme.css']`. Paths are resolved from the project root. + */ + customCss?: string[]; +} const VIRTUAL_CONFIG_ID = 'virtual:starpod/config'; const RESOLVED_VIRTUAL_CONFIG_ID = '\0' + VIRTUAL_CONFIG_ID; +const VIRTUAL_USER_CSS_ID = 'virtual:starpod/user-css'; +const RESOLVED_VIRTUAL_USER_CSS_ID = '\0' + VIRTUAL_USER_CSS_ID; +const VIRTUAL_COMPONENT_PREFIX = 'virtual:starpod/components/'; /** - * Exposes the user's Starpod config to every page and component in the - * package as the `virtual:starpod/config` module. + * Serves the virtual modules that connect the consuming site to the package: + * the validated config (plus feature flags), user CSS, and component + * overrides. */ -function starpodConfigPlugin(config: StarpodConfig): VitePlugin { +function starpodVitePlugin( + config: StarpodConfig, + options: StarpodOptions, + root: URL +): VitePlugin { + const rootDir = fileURLToPath(root); + return { - name: 'starpod-config', + name: 'starpod', resolveId(id) { if (id === VIRTUAL_CONFIG_ID) { return RESOLVED_VIRTUAL_CONFIG_ID; } + if (id === VIRTUAL_USER_CSS_ID) { + return RESOLVED_VIRTUAL_USER_CSS_ID; + } + if (id.startsWith(VIRTUAL_COMPONENT_PREFIX)) { + const name = id.slice( + VIRTUAL_COMPONENT_PREFIX.length + ) as OverridableComponent; + const defaultPath = OVERRIDABLE_COMPONENTS[name]; + if (!defaultPath) { + throw new Error( + `[starpod] Unknown component "${name}". Overridable components: ${Object.keys(OVERRIDABLE_COMPONENTS).join(', ')}` + ); + } + const override = options.components?.[name]; + return override + ? resolvePath(rootDir, override) + : fileURLToPath(new URL(defaultPath, import.meta.url)); + } }, load(id) { if (id === RESOLVED_VIRTUAL_CONFIG_ID) { - return `export default ${JSON.stringify(config)};`; + return [ + `export default ${JSON.stringify(config)};`, + `export const database = ${options.database === true};` + ].join('\n'); + } + if (id === RESOLVED_VIRTUAL_USER_CSS_ID) { + const imports = (options.customCss ?? []).map( + (css) => + `import ${JSON.stringify( + css.startsWith('.') ? resolvePath(rootDir, css) : css + )};` + ); + return imports.join('\n') || 'export {};'; } } }; @@ -65,7 +149,10 @@ const ROUTES: Array<{ pattern: string; entrypoint: string }> = [ } ]; -export default function starpod(starpodConfig: StarpodConfig): AstroIntegration { +export default function starpod( + starpodConfig: StarpodConfig, + options: StarpodOptions = {} +): AstroIntegration { let projectRoot: URL; return { @@ -110,7 +197,13 @@ export default function starpod(starpodConfig: StarpodConfig): AstroIntegration remotePatterns: [{ protocol: 'https' }, { protocol: 'http' }] }, integrations: [ - preact(), + preact({ + // The preset's default exclude is /node_modules/, which would + // skip this package's own .tsx components when starpod is + // installed as a dependency. Exclude everything else but keep + // starpod's sources in the Preact JSX transform. + exclude: [/node_modules(?!.*[\\/]starpod[\\/])/] + }), sitemap({ filter: (page) => { const pathname = new URL(page).pathname; @@ -130,21 +223,28 @@ export default function starpod(starpodConfig: StarpodConfig): AstroIntegration }, trailingSlash: 'never', vite: { - plugins: [tailwindcss(), starpodConfigPlugin(starpodConfig)] + plugins: [ + tailwindcss(), + starpodVitePlugin(starpodConfig, options, config.root) + ] } }); }, - 'astro:build:done': () => { + 'astro:build:done': ({ dir, assets }) => { // Injects `Accept: text/markdown` content-negotiation routes into the - // Vercel build output. No-ops for other adapters. The adapter's own - // astro:build:done hook runs after this one and writes - // `.vercel/output/config.json`, so the patch must wait until process - // exit (the patch is synchronous fs work, which is safe there). - process.once('exit', () => { - applyVercelMarkdownNegotiation( - fileURLToPath(new URL('./.vercel/output', projectRoot)) - ); - }); + // Vercel build output. No-ops for other adapters. The adapter runs + // first in astro:build:done (Astro unshifts it), so config.json exists + // here — but the static dir is only copied afterwards, so the markdown + // twin paths are derived from the build's generated assets, not the + // filesystem. + const outputBase = fileURLToPath(dir); + const generated = [...assets.values()] + .flat() + .map((url) => fileURLToPath(url).slice(outputBase.length)); + applyVercelMarkdownNegotiation( + fileURLToPath(new URL('./.vercel/output', projectRoot)), + pagesToMarkdownPaths(generated) + ); } } }; diff --git a/packages/starpod/src/layouts/Layout.astro b/packages/starpod/src/layouts/Layout.astro index ffa86e4f..ba36cc08 100644 --- a/packages/starpod/src/layouts/Layout.astro +++ b/packages/starpod/src/layouts/Layout.astro @@ -6,19 +6,22 @@ import { Schema } from 'astro-seo-schema'; import SpeedInsights from '@vercel/speed-insights/astro'; import Breadcrumbs from '../components/Breadcrumbs.astro'; -import Dots from '../components/Dots.astro'; -import Hosts from '../components/Hosts.astro'; -import InfoCard from '../components/InfoCard.astro'; -import Platforms from '../components/Platforms.astro'; +import Dots from 'virtual:starpod/components/Dots'; +import Hosts from 'virtual:starpod/components/Hosts'; +import InfoCard from 'virtual:starpod/components/InfoCard'; +import Platforms from 'virtual:starpod/components/Platforms'; import Player from '../components/Player'; import SearchButton from '../components/SearchButton'; import SearchDialog from '../components/SearchDialog'; -import ShowArtwork from '../components/ShowArtwork.astro'; +import ShowArtwork from 'virtual:starpod/components/ShowArtwork'; import { getShowInfo } from '../lib/rss'; import starpodConfig from 'virtual:starpod/config'; import '../styles/global.css'; +// Site-provided stylesheets from the integration's `customCss` option; loads +// after the built-in styles so they win the cascade. +import 'virtual:starpod/user-css'; const show = await getShowInfo(); diff --git a/packages/starpod/src/lib/rss.ts b/packages/starpod/src/lib/rss.ts index fa5a47fa..91670ce4 100644 --- a/packages/starpod/src/lib/rss.ts +++ b/packages/starpod/src/lib/rss.ts @@ -15,6 +15,17 @@ import type { StarpodConfig } from '../utils/config'; import { dasherize } from '../utils/dasherize'; import { truncate } from '../utils/truncate'; +// rss-to-json is CJS with `__esModule: true` and both `exports.parse` and +// `exports.default` pointing at the same function, so what the default import +// yields depends on whether the bundler externalized it. Normalize both +// shapes to the parse function. +type ParseFeedFn = (url: string) => Promise; +const parseFeedModule: unknown = parseFeed; +const parseRssFeed: ParseFeedFn = + typeof parseFeedModule === 'function' + ? (parseFeedModule as ParseFeedFn) + : (parseFeedModule as { parse: ParseFeedFn }).parse; + // Inside Astro (dev, build, vitest) the config comes from the integration's // virtual module. Standalone scripts (e.g. `db/seed.ts` under tsx) have no // Vite, so they must pass their config explicitly and the dynamic import is @@ -92,8 +103,7 @@ export async function getShowInfo(config?: StarpodConfig) { return showInfoCache; } - // @ts-expect-error - const showInfo = (await parseFeed.parse(await resolveRssFeed(config))) as Show; + const showInfo = (await parseRssFeed(await resolveRssFeed(config))) as Show; showInfo.image = (await optimizeImage(showInfo.image, { height: 640, width: 640 @@ -136,8 +146,7 @@ export async function getAllEpisodes(config?: StarpodConfig) { ) }); - // @ts-expect-error - let feed = (await parseFeed.parse(await resolveRssFeed(config))) as Show; + let feed = (await parseRssFeed(await resolveRssFeed(config))) as Show; let items = parse(FeedSchema, feed).items; let episodes: Array = await Promise.all( diff --git a/packages/starpod/src/pages/404.astro b/packages/starpod/src/pages/404.astro index 1cadd86c..49fb4289 100644 --- a/packages/starpod/src/pages/404.astro +++ b/packages/starpod/src/pages/404.astro @@ -1,6 +1,6 @@ --- import Layout from '../layouts/Layout.astro'; -import NotFoundContent from '../components/NotFoundContent.astro'; +import NotFoundContent from 'virtual:starpod/components/NotFoundContent'; --- diff --git a/packages/starpod/src/pages/[...notFound].astro b/packages/starpod/src/pages/[...notFound].astro index 0170f24b..335c54db 100644 --- a/packages/starpod/src/pages/[...notFound].astro +++ b/packages/starpod/src/pages/[...notFound].astro @@ -1,6 +1,6 @@ --- import Layout from '../layouts/Layout.astro'; -import NotFoundContent from '../components/NotFoundContent.astro'; +import NotFoundContent from 'virtual:starpod/components/NotFoundContent'; import { jsonError } from '../lib/api-errors'; import { generateNotFoundMarkdown } from '../lib/not-found'; import { getShowInfo } from '../lib/rss'; diff --git a/packages/starpod/src/pages/[episode].astro b/packages/starpod/src/pages/[episode].astro index cbb99e0f..e70d3396 100644 --- a/packages/starpod/src/pages/[episode].astro +++ b/packages/starpod/src/pages/[episode].astro @@ -23,6 +23,8 @@ import FullPlayButton from '../components/FullPlayButton'; import UFOIllustration from '../components/illustrations/UFOIllustration.astro'; import { generateDocumentLinkTag } from '@bryanguffey/astro-standard-site'; +import { database } from 'virtual:starpod/config'; + import Layout from '../layouts/Layout.astro'; import { getAllEpisodes, getShowInfo } from '../lib/rss'; import { @@ -83,37 +85,45 @@ const playerEpisode = { const canonicalURL = new URL(`/${episode.episodeSlug}`, Astro.url); -const db = createDb( - import.meta.env.ASTRO_DB_REMOTE_URL, - import.meta.env.ASTRO_DB_APP_TOKEN -); - -const hostsAndGuests = await db - .select({ - id: Person.id, - img: Person.img, - isHost: HostOrGuest.isHost, - name: Person.name - }) - .from(HostOrGuest) - .innerJoin(DbEpisode, eq(HostOrGuest.episodeSlug, DbEpisode.episodeSlug)) - .innerJoin(Person, eq(HostOrGuest.personId, Person.id)) - .where(eq(DbEpisode.episodeSlug, episode.episodeSlug)); - -const sponsors = await db - .select({ - id: Sponsor.id, - img: Sponsor.img, - name: Sponsor.name, - url: Sponsor.url - }) - .from(SponsorForEpisode) - .innerJoin( - DbEpisode, - eq(SponsorForEpisode.episodeSlug, DbEpisode.episodeSlug) - ) - .innerJoin(Sponsor, eq(SponsorForEpisode.sponsorId, Sponsor.id)) - .where(eq(DbEpisode.episodeSlug, episode.episodeSlug)); +// The guests/sponsors database is opt-in via the integration's `database` +// option; without it the page renders with those sections absent. +const db = database + ? createDb( + import.meta.env.ASTRO_DB_REMOTE_URL, + import.meta.env.ASTRO_DB_APP_TOKEN + ) + : null; + +const hostsAndGuests = !db + ? [] + : await db + .select({ + id: Person.id, + img: Person.img, + isHost: HostOrGuest.isHost, + name: Person.name + }) + .from(HostOrGuest) + .innerJoin(DbEpisode, eq(HostOrGuest.episodeSlug, DbEpisode.episodeSlug)) + .innerJoin(Person, eq(HostOrGuest.personId, Person.id)) + .where(eq(DbEpisode.episodeSlug, episode.episodeSlug)); + +const sponsors = !db + ? [] + : await db + .select({ + id: Sponsor.id, + img: Sponsor.img, + name: Sponsor.name, + url: Sponsor.url + }) + .from(SponsorForEpisode) + .innerJoin( + DbEpisode, + eq(SponsorForEpisode.episodeSlug, DbEpisode.episodeSlug) + ) + .innerJoin(Sponsor, eq(SponsorForEpisode.sponsorId, Sponsor.id)) + .where(eq(DbEpisode.episodeSlug, episode.episodeSlug)); const title = `${episode.title} - ${show.title} - Episode ${episode.episodeNumber}`; diff --git a/packages/starpod/src/pages/about.astro b/packages/starpod/src/pages/about.astro index c83bf3e6..32356e13 100644 --- a/packages/starpod/src/pages/about.astro +++ b/packages/starpod/src/pages/about.astro @@ -4,12 +4,19 @@ import { Image } from 'astro:assets'; import Layout from '../layouts/Layout.astro'; import starpodConfig from 'virtual:starpod/config'; -import LargePlatforms from '../components/LargePlatforms.astro'; +import LargePlatforms from 'virtual:starpod/components/LargePlatforms'; + +import avatarLight from '../assets/avatar-light.png'; const { hosts } = starpodConfig; const images = import.meta.glob<{ default: ImageMetadata }>( '/src/img/people/*.{jpeg,jpg,png}' ); + +// Fall back to the packaged avatar when the site has not provided a photo in +// src/img/people yet. +const hostImage = (img: string) => + images[`/src/img/people/${img}`]?.() ?? avatarLight; --- @@ -36,7 +43,7 @@ const images = import.meta.glob<{ default: ImageMetadata }>( diff --git a/packages/starpod/src/pages/index.astro b/packages/starpod/src/pages/index.astro index 6a4f6489..57577192 100644 --- a/packages/starpod/src/pages/index.astro +++ b/packages/starpod/src/pages/index.astro @@ -1,6 +1,6 @@ --- import Layout from '../layouts/Layout.astro'; -import EpisodeList from '../components/EpisodeList.astro'; +import EpisodeList from 'virtual:starpod/components/EpisodeList'; import { getAllEpisodes, getShowInfo } from '../lib/rss'; const episodes = await getAllEpisodes(); const show = await getShowInfo(); diff --git a/packages/starpod/src/pages/sponsor.astro b/packages/starpod/src/pages/sponsor.astro index e64a560c..fe646057 100644 --- a/packages/starpod/src/pages/sponsor.astro +++ b/packages/starpod/src/pages/sponsor.astro @@ -12,13 +12,16 @@ const countryFlags = import.meta.glob<{ default: ImageMetadata }>( '/src/img/countries/*.{jpeg,jpg,png}' ); +// TODO(starpod): this page's copy, ad packages, and listener stats are still +// Whiskey-specific and should move into starpod.config. Rows whose flag image +// is missing from src/img/countries are skipped so fresh sites don't crash. const countries = [ { name: 'United States', flag: 'USA.png', percentage: 52 }, { name: 'United Kingdom', flag: 'UK.png', percentage: 7 }, { name: 'Canada', flag: 'Canada.png', percentage: 5 }, { name: 'Germany', flag: 'Germany.png', percentage: 4 }, { name: 'Australia', flag: 'Australia.png', percentage: 4 } -]; +].filter((country) => countryFlags[`/src/img/countries/${country.flag}`]); --- diff --git a/packages/starpod/src/styles/search-icon.css b/packages/starpod/src/styles/search-icon.css index 33a130b4..b1e8f090 100644 --- a/packages/starpod/src/styles/search-icon.css +++ b/packages/starpod/src/styles/search-icon.css @@ -1,5 +1,5 @@ .search-icon { - mask-image: url(/images/search-icon.svg); + mask-image: url('../assets/search-icon.svg'); mask-size: contain; mask-repeat: no-repeat; mask-position: center; diff --git a/packages/starpod/src/styles/tailwind.css b/packages/starpod/src/styles/tailwind.css index 6219eb38..dcb26567 100644 --- a/packages/starpod/src/styles/tailwind.css +++ b/packages/starpod/src/styles/tailwind.css @@ -13,8 +13,8 @@ @plugin '@tailwindcss/typography'; @theme { - --background-image-dots-dark: url('/images/dots-dark.svg'); - --background-image-dots-light: url('/images/dots-light.svg'); + --background-image-dots-dark: url('../assets/dots-dark.svg'); + --background-image-dots-light: url('../assets/dots-light.svg'); --background-image-gradient-light: background: radial-gradient( 77% 77% at 50% 0%, #edecfc 0%, diff --git a/packages/starpod/src/vercel-md-negotiation.d.mts b/packages/starpod/src/vercel-md-negotiation.d.mts index 0e35565b..bc6112db 100644 --- a/packages/starpod/src/vercel-md-negotiation.d.mts +++ b/packages/starpod/src/vercel-md-negotiation.d.mts @@ -18,9 +18,10 @@ interface VercelOutputConfig { } export function collectMarkdownPaths(staticDir: string): string[]; +export function pagesToMarkdownPaths(pathnames: string[]): string[]; export function buildNegotiationRoutes(mdPaths: string[]): NegotiationRoute[]; export function patchConfig( config: VercelOutputConfig, mdPaths: string[] ): { config: VercelOutputConfig; inserted: number }; -export function main(outputDir?: string): void; +export function main(outputDir?: string, mdPaths?: string[] | null): void; diff --git a/packages/starpod/src/vercel-md-negotiation.mjs b/packages/starpod/src/vercel-md-negotiation.mjs index ca18f9b3..cf3bac45 100644 --- a/packages/starpod/src/vercel-md-negotiation.mjs +++ b/packages/starpod/src/vercel-md-negotiation.mjs @@ -54,6 +54,20 @@ export function collectMarkdownPaths(staticDir) { return paths.sort(); } +/** + * Derive negotiated URL paths from Astro's generated page pathnames (as given + * to the `astro:build:done` hook), e.g. 'about.html.md' -> '/about'. Used by + * the integration, where the static output directory is not yet populated at + * hook time. + */ +export function pagesToMarkdownPaths(pathnames) { + return pathnames + .map((p) => p.replace(/^\/+/, '').replace(/\/+$/, '')) + .filter((p) => p.endsWith(MD_SUFFIX)) + .map((p) => '/' + p.slice(0, -MD_SUFFIX.length)) + .sort(); +} + const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const chunk = (items, size) => { @@ -171,11 +185,11 @@ export function patchConfig(config, mdPaths) { return { config: { ...config, routes }, inserted: negotiationRoutes.length }; } -export function main(outputDir = '.vercel/output') { +export function main(outputDir = '.vercel/output', mdPaths = null) { const configPath = join(outputDir, 'config.json'); const staticDir = join(outputDir, 'static'); - if (!existsSync(configPath) || !existsSync(staticDir)) { + if (!existsSync(configPath) || (mdPaths === null && !existsSync(staticDir))) { console.log( `[md-negotiation] No Vercel build output at ${outputDir}, skipping` ); @@ -183,7 +197,9 @@ export function main(outputDir = '.vercel/output') { } const config = JSON.parse(readFileSync(configPath, 'utf-8')); - const mdPaths = collectMarkdownPaths(staticDir); + if (mdPaths === null) { + mdPaths = collectMarkdownPaths(staticDir); + } const { config: patched, inserted } = patchConfig(config, mdPaths); if (inserted === 0) { diff --git a/packages/starpod/src/virtual.d.ts b/packages/starpod/src/virtual.d.ts index cb95a312..01018735 100644 --- a/packages/starpod/src/virtual.d.ts +++ b/packages/starpod/src/virtual.d.ts @@ -1,4 +1,16 @@ declare module 'virtual:starpod/config' { const config: import('./utils/config').StarpodConfig; export default config; + /** Whether the guests/sponsors database was enabled via integration options. */ + export const database: boolean; +} + +declare module 'virtual:starpod/user-css' { + const noop: Record; + export default noop; +} + +declare module 'virtual:starpod/components/*' { + const Component: (props: Record) => unknown; + export default Component; } diff --git a/packages/starpod/tsconfig.json b/packages/starpod/tsconfig.json new file mode 100644 index 00000000..4d3cf63a --- /dev/null +++ b/packages/starpod/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strictNullChecks": true, + "verbatimModuleSyntax": true, + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4e269be..4f670df3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,9 +117,6 @@ importers: packages/starpod: dependencies: - '@astrojs/preact': - specifier: ^5.1.4 - version: 5.1.4(@babel/core@7.29.7)(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(preact@10.29.2)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) '@astrojs/sitemap': specifier: ^3.7.3 version: 3.7.3 @@ -156,9 +153,6 @@ importers: html-to-text: specifier: ^9.0.5 version: 9.0.5 - preact: - specifier: ^10.29.2 - version: 10.29.2 rss-to-json: specifier: ^2.1.1 version: 2.1.1 @@ -172,12 +166,18 @@ importers: specifier: ^1.4.1 version: 1.4.1(typescript@5.9.3) devDependencies: + '@astrojs/preact': + specifier: ^5.1.4 + version: 5.1.4(@babel/core@7.29.7)(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(preact@10.29.2)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) '@types/html-to-text': specifier: ^9.0.4 version: 9.0.4 astro: specifier: 6.4.2 version: 6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) + preact: + specifier: ^10.29.2 + version: 10.29.2 typescript: specifier: ^5.9.3 version: 5.9.3 diff --git a/tsconfig.json b/tsconfig.json index d6e44e15..27b3b965 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "jsxImportSource": "preact" }, "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"] + "exclude": ["dist", "packages/create-starpod/template"] } From 2a223ddbf8d266a703946f825e9f92db522fe322 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 21:26:42 -0400 Subject: [PATCH 04/12] Add create-starpod scaffolder npm create starpod: prompts for name/RSS/database (or flags --name --rss --database --yes) and generates a ready-to-build consumer project: renderer deps declared directly, tsconfig with starpod/src/* paths for tests, content config, .env.example, optional db/ folder with empty data files, seed script, and drizzle config, plus the fast-xml-parser pin and sharp that real feeds need. Smoke-tested against the local package with a component override and customCss. Co-Authored-By: Claude Fable 5 --- packages/create-starpod/index.mjs | 236 ++++++++++++++++++ packages/create-starpod/package.json | 20 ++ .../create-starpod/template/base/README.md | 47 ++++ .../create-starpod/template/base/env.example | 7 + .../create-starpod/template/base/gitignore | 20 ++ .../template/base/public/.gitkeep | 0 .../template/base/src/content.config.ts | 10 + .../base/src/content/transcripts/.gitkeep | 0 .../template/base/src/img/people/.gitkeep | 0 .../template/base/tsconfig.json | 17 ++ .../database/db/data/people-per-episode.ts | 12 + .../template/database/db/data/people.ts | 9 + .../database/db/data/sponsors-per-episode.ts | 6 + .../template/database/db/data/sponsors.ts | 7 + .../template/database/db/seed.ts | 99 ++++++++ .../template/database/drizzle.config.ts | 12 + 16 files changed, 502 insertions(+) create mode 100755 packages/create-starpod/index.mjs create mode 100644 packages/create-starpod/package.json create mode 100644 packages/create-starpod/template/base/README.md create mode 100644 packages/create-starpod/template/base/env.example create mode 100644 packages/create-starpod/template/base/gitignore create mode 100644 packages/create-starpod/template/base/public/.gitkeep create mode 100644 packages/create-starpod/template/base/src/content.config.ts create mode 100644 packages/create-starpod/template/base/src/content/transcripts/.gitkeep create mode 100644 packages/create-starpod/template/base/src/img/people/.gitkeep create mode 100644 packages/create-starpod/template/base/tsconfig.json create mode 100644 packages/create-starpod/template/database/db/data/people-per-episode.ts create mode 100644 packages/create-starpod/template/database/db/data/people.ts create mode 100644 packages/create-starpod/template/database/db/data/sponsors-per-episode.ts create mode 100644 packages/create-starpod/template/database/db/data/sponsors.ts create mode 100644 packages/create-starpod/template/database/db/seed.ts create mode 100644 packages/create-starpod/template/database/drizzle.config.ts diff --git a/packages/create-starpod/index.mjs b/packages/create-starpod/index.mjs new file mode 100755 index 00000000..43040de3 --- /dev/null +++ b/packages/create-starpod/index.mjs @@ -0,0 +1,236 @@ +#!/usr/bin/env node +/** + * create-starpod: scaffold a podcast website powered by the starpod Astro + * integration. Dependency-free on purpose — prompts use node:readline. + * + * Usage: + * npm create starpod@latest [dir] [--name "My Show"] [--rss ] + * [--database] [--yes] + */ + +import { + cpSync, + existsSync, + mkdirSync, + readdirSync, + renameSync, + writeFileSync +} from 'node:fs'; +import { join, resolve } from 'node:path'; +import process from 'node:process'; +import readline from 'node:readline/promises'; +import { fileURLToPath } from 'node:url'; + +const TEMPLATE_DIR = fileURLToPath(new URL('./template', import.meta.url)); + +const args = process.argv.slice(2); +const flags = new Set(args.filter((a) => a.startsWith('--'))); +const positional = args.filter((a) => !a.startsWith('--')); + +const flagValue = (name) => { + const index = args.indexOf(name); + return index !== -1 && args[index + 1] && !args[index + 1].startsWith('--') + ? args[index + 1] + : undefined; +}; + +// A --flag with a following value keeps that value out of positionals. +for (const name of ['--name', '--rss']) { + const value = flagValue(name); + if (value) { + positional.splice(positional.indexOf(value), 1); + } +} + +const dasherize = (value) => + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'my-podcast'; + +async function collectAnswers() { + const skipPrompts = flags.has('--yes'); + const rl = skipPrompts + ? null + : readline.createInterface({ input: process.stdin, output: process.stdout }); + + const ask = async (question, fallback) => { + if (!rl) { + return fallback; + } + const answer = (await rl.question(`${question} (${fallback}) `)).trim(); + return answer || fallback; + }; + + const name = + flagValue('--name') ?? (await ask('What is your podcast called?', 'My Podcast')); + const dir = positional[0] ?? (await ask('Where should we create it?', dasherize(name))); + const rssFeed = + flagValue('--rss') ?? + (await ask('RSS feed URL?', 'https://example.com/feed.xml')); + const database = flags.has('--database') + ? true + : skipPrompts + ? false + : (await ask('Enable the guests & sponsors database (Turso)?', 'no')) + .toLowerCase() + .startsWith('y'); + + rl?.close(); + return { name, dir, rssFeed, database }; +} + +function generatePackageJson({ name, database }) { + const pkg = { + name: dasherize(name), + private: true, + type: 'module', + version: '0.0.1', + scripts: { + astro: 'astro', + build: 'astro check && astro build', + dev: 'astro dev', + preview: 'astro preview', + ...(database && { + 'db:push': 'drizzle-kit push', + 'db:seed': 'tsx db/seed.ts', + 'db:studio': 'drizzle-kit studio' + }) + }, + dependencies: { + '@astrojs/preact': '^5.1.4', + '@astrojs/vercel': '^10.0.8', + astro: '^6.4.2', + preact: '^10.29.2', + starpod: '^0.1.0' + }, + devDependencies: { + '@astrojs/check': '^0.9.9', + sharp: '^0.34.5', + typescript: '^5.9.3', + ...(database && { + dotenv: '^17.4.2', + 'drizzle-kit': '^0.31.10', + 'drizzle-orm': '^0.45.2', + tsx: '^4.22.4' + }) + }, + // rss-to-json hardcodes its parser options, and newer fast-xml-parser + // versions enforce an entity-expansion limit that real podcast feeds + // exceed. Pin the version rss-to-json was built against. + // (pnpm reads `pnpm.overrides`; npm reads `overrides`.) + pnpm: { + overrides: { + 'fast-xml-parser': '4.5.4' + }, + onlyBuiltDependencies: ['@tailwindcss/oxide', 'esbuild', 'sharp'] + }, + overrides: { + 'fast-xml-parser': '4.5.4' + } + }; + return JSON.stringify(pkg, null, 2) + '\n'; +} + +function generateStarpodConfig({ name, rssFeed }) { + return `import { defineStarpodConfig } from 'starpod/config'; + +export default defineStarpodConfig({ + blurb: 'A short one-line tagline for ${name}.', + description: + 'A couple of sentences describing what ${name} is about. Shown on the about page and in metadata.', + hosts: [ + { + name: 'Your Name', + bio: 'A short bio.', + // Drop a photo into src/img/people/ and reference its filename here. + img: 'your-name.jpg' + } + ], + platforms: { + // Add the platforms your show is published on; unset ones are hidden. + // apple: 'https://podcasts.apple.com/us/podcast/your-show/id0000000000', + // spotify: 'https://open.spotify.com/show/...', + // overcast: 'https://overcast.fm/itunes0000000000', + // pocketCasts: 'https://pca.st/...', + // youtube: 'https://www.youtube.com/@yourshow' + }, + rssFeed: '${rssFeed}' +}); +`; +} + +function generateAstroConfig({ database }) { + return `import vercel from '@astrojs/vercel'; +import { defineConfig } from 'astro/config'; +import starpod from 'starpod'; + +import starpodConfig from './starpod.config'; + +// https://astro.build/config +export default defineConfig({ + output: 'static', + adapter: vercel(), + // Set this to your production URL. + site: 'https://example.com', + integrations: [ + starpod(starpodConfig${ + database + ? `, { + // Per-episode guests and sponsors from Turso. Requires + // ASTRO_DB_REMOTE_URL and ASTRO_DB_APP_TOKEN (see .env.example). + database: true + }` + : `, { + // Options you can enable later: + // database: true, // per-episode guests/sponsors via Turso + Drizzle + // components: { Hosts: './src/components/MyHosts.astro' }, + // customCss: ['./src/styles/theme.css'] + }` + }) + ] +}); +`; +} + +async function main() { + const answers = await collectAnswers(); + const target = resolve(process.cwd(), answers.dir); + + if (existsSync(target) && readdirSync(target).length > 0) { + console.error(`\n✖ ${answers.dir} already exists and is not empty.`); + process.exit(1); + } + + mkdirSync(target, { recursive: true }); + cpSync(join(TEMPLATE_DIR, 'base'), target, { recursive: true }); + if (answers.database) { + cpSync(join(TEMPLATE_DIR, 'database'), target, { recursive: true }); + } + + // npm strips dotfiles from published packages, so these ship undotted. + renameSync(join(target, 'gitignore'), join(target, '.gitignore')); + renameSync(join(target, 'env.example'), join(target, '.env.example')); + + writeFileSync(join(target, 'package.json'), generatePackageJson(answers)); + writeFileSync(join(target, 'starpod.config.ts'), generateStarpodConfig(answers)); + writeFileSync(join(target, 'astro.config.mjs'), generateAstroConfig(answers)); + + console.log(`\n✔ Scaffolded ${answers.name} in ${answers.dir}\n`); + console.log('Next steps:'); + console.log(` cd ${answers.dir}`); + console.log(' pnpm install (or npm install / yarn)'); + console.log(' pnpm dev'); + if (answers.database) { + console.log('\nDatabase setup:'); + console.log(' 1. Create a Turso database and copy .env.example to .env'); + console.log(' 2. pnpm db:push (create the schema)'); + console.log(' 3. Edit db/data/*.ts, then pnpm db:seed'); + } + console.log(''); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/create-starpod/package.json b/packages/create-starpod/package.json new file mode 100644 index 00000000..3726acff --- /dev/null +++ b/packages/create-starpod/package.json @@ -0,0 +1,20 @@ +{ + "name": "create-starpod", + "type": "module", + "version": "0.1.0", + "description": "Scaffold a podcast website powered by the starpod Astro integration.", + "repository": { + "type": "git", + "url": "git+https://github.com/shipshapecode/starpod.git", + "directory": "packages/create-starpod" + }, + "license": "MIT", + "keywords": ["astro", "podcast", "starpod", "create"], + "bin": { + "create-starpod": "./index.mjs" + }, + "files": ["index.mjs", "template"], + "engines": { + "node": "^22.0.0" + } +} diff --git a/packages/create-starpod/template/base/README.md b/packages/create-starpod/template/base/README.md new file mode 100644 index 00000000..475cc399 --- /dev/null +++ b/packages/create-starpod/template/base/README.md @@ -0,0 +1,47 @@ +# Your Starpod site + +A podcast website powered by [Starpod](https://github.com/shipshapecode/starpod), +an Astro integration that turns an RSS feed into a full site: episode pages, +transcripts with clickable timestamps, search, an audio player, LLM-friendly +markdown twins of every page, and an OpenAPI-documented JSON API. + +## Commands + +- `pnpm dev` — start the dev server on localhost:4321 +- `pnpm build` — type-check and build for production +- `pnpm preview` — preview the production build + +## Configure your show + +Everything starts in `starpod.config.ts`: title blurb, description, hosts, and +the RSS feed URL. The site rebuilds its episode pages from the feed on every +build. + +- **Host photos** go in `src/img/people/` and are referenced by filename from + the `hosts` array in `starpod.config.ts`. A placeholder avatar is shown for + any missing photo. +- **Favicons** and other static files go in `public/`. +- **Transcripts**: drop `.md` files into + `src/content/transcripts/`. `[HH:MM:SS]` timestamps become clickable and + seek the player. Without a file, the site falls back to the transcript + referenced by the feed's `` tag. + +## Options + +Pass options as the second argument to `starpod()` in `astro.config.mjs`: + +- `database: true` — per-episode guests and sponsors stored in + [Turso](https://turso.tech) (see `.env.example`). When off, those sections + simply don't render. +- `components: { Hosts: './src/components/MyHosts.astro' }` — replace built-in + components with your own. Overridable: Dots, EpisodeList, Hosts, InfoCard, + LargePlatforms, NotFoundContent, Platforms, ShowArtwork. +- `customCss: ['./src/styles/theme.css']` — stylesheets loaded after the + built-in styles, for theming via CSS custom properties. + +## Writing your own tests + +The `tsconfig.json` maps `starpod/src/*` to the package source, so unit tests +can import any engine module directly, e.g. +`import { getAllEpisodes } from 'starpod/src/lib/rss';`. If you use Vitest, +add the same mapping as a `resolve.alias` entry in `vitest.config.ts`. diff --git a/packages/create-starpod/template/base/env.example b/packages/create-starpod/template/base/env.example new file mode 100644 index 00000000..96a3fc31 --- /dev/null +++ b/packages/create-starpod/template/base/env.example @@ -0,0 +1,7 @@ +# Contact form: Discord webhook the contact API posts to. +DISCORD_WEBHOOK= + +# Guests & sponsors database (only with `database: true` in astro.config.mjs). +# Create a database at https://turso.tech and fill these in. +ASTRO_DB_REMOTE_URL= +ASTRO_DB_APP_TOKEN= diff --git a/packages/create-starpod/template/base/gitignore b/packages/create-starpod/template/base/gitignore new file mode 100644 index 00000000..bfc093c5 --- /dev/null +++ b/packages/create-starpod/template/base/gitignore @@ -0,0 +1,20 @@ +# build output +dist/ +.vercel/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS +.DS_Store diff --git a/packages/create-starpod/template/base/public/.gitkeep b/packages/create-starpod/template/base/public/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/create-starpod/template/base/src/content.config.ts b/packages/create-starpod/template/base/src/content.config.ts new file mode 100644 index 00000000..0e0c7d44 --- /dev/null +++ b/packages/create-starpod/template/base/src/content.config.ts @@ -0,0 +1,10 @@ +import { defineCollection } from 'astro:content'; +import { transcriptsLoader } from 'starpod/content'; + +const transcripts = defineCollection({ + loader: transcriptsLoader() +}); + +export const collections = { + transcripts +}; diff --git a/packages/create-starpod/template/base/src/content/transcripts/.gitkeep b/packages/create-starpod/template/base/src/content/transcripts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/create-starpod/template/base/src/img/people/.gitkeep b/packages/create-starpod/template/base/src/img/people/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/create-starpod/template/base/tsconfig.json b/packages/create-starpod/template/base/tsconfig.json new file mode 100644 index 00000000..019422ad --- /dev/null +++ b/packages/create-starpod/template/base/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "starpod/src/*": ["./node_modules/starpod/src/*"] + }, + "esModuleInterop": true, + "strictNullChecks": true, + "verbatimModuleSyntax": true, + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/packages/create-starpod/template/database/db/data/people-per-episode.ts b/packages/create-starpod/template/database/db/data/people-per-episode.ts new file mode 100644 index 00000000..b9d45ab1 --- /dev/null +++ b/packages/create-starpod/template/database/db/data/people-per-episode.ts @@ -0,0 +1,12 @@ +import people from './people'; + +type PersonId = (typeof people)[number]['id']; + +/** + * Who appears on which episode, keyed by episode slug. Hosts are listed in + * starpod.config.ts; set `isHost` on entries that are hosts so they sort + * first. + */ +export default { + // 'my-first-episode-slug': [{ id: 'jane-doe' }] +} as Record>; diff --git a/packages/create-starpod/template/database/db/data/people.ts b/packages/create-starpod/template/database/db/data/people.ts new file mode 100644 index 00000000..a1e8b17a --- /dev/null +++ b/packages/create-starpod/template/database/db/data/people.ts @@ -0,0 +1,9 @@ +/** + * Guests (and hosts) who appear on episodes. `img` is a filename inside + * src/img/people/; omit it to show the placeholder avatar. + */ +export const people = [ + // { id: 'jane-doe', name: 'Jane Doe', img: 'jane-doe.jpg' } +]; + +export default people; diff --git a/packages/create-starpod/template/database/db/data/sponsors-per-episode.ts b/packages/create-starpod/template/database/db/data/sponsors-per-episode.ts new file mode 100644 index 00000000..ce0c8c75 --- /dev/null +++ b/packages/create-starpod/template/database/db/data/sponsors-per-episode.ts @@ -0,0 +1,6 @@ +/** + * Which sponsors ran on which episode, keyed by episode slug. + */ +export default { + // 'my-first-episode-slug': [{ id: 'acme' }] +} as Record>; diff --git a/packages/create-starpod/template/database/db/data/sponsors.ts b/packages/create-starpod/template/database/db/data/sponsors.ts new file mode 100644 index 00000000..84670ed9 --- /dev/null +++ b/packages/create-starpod/template/database/db/data/sponsors.ts @@ -0,0 +1,7 @@ +/** + * Sponsors that can appear on episodes. `img` is a filename inside + * src/img/sponsors/. + */ +export default [ + // { id: 'acme', name: 'Acme Corp', img: 'acme.svg', url: 'https://acme.com' } +]; diff --git a/packages/create-starpod/template/database/db/seed.ts b/packages/create-starpod/template/database/db/seed.ts new file mode 100644 index 00000000..51a380c7 --- /dev/null +++ b/packages/create-starpod/template/database/db/seed.ts @@ -0,0 +1,99 @@ +import 'dotenv/config'; + +import { sql } from 'drizzle-orm'; + +import { createDb } from 'starpod/db'; +import { + Episode, + HostOrGuest, + Person, + Sponsor, + SponsorForEpisode +} from 'starpod/db/schema'; +import { getAllEpisodes } from 'starpod/src/lib/rss'; + +import starpodConfig from '../starpod.config'; +import people from './data/people'; +import peoplePerEpisode from './data/people-per-episode'; +import sponsors from './data/sponsors'; +import sponsorsPerEpisode from './data/sponsors-per-episode'; + +const db = createDb( + process.env.ASTRO_DB_REMOTE_URL!, + process.env.ASTRO_DB_APP_TOKEN! +); + +async function seed() { + if (people.length) { + await db + .insert(Person) + .values(people) + .onConflictDoUpdate({ + target: Person.id, + set: { name: sql`excluded.name`, img: sql`excluded.img` } + }); + } + + if (sponsors.length) { + await db + .insert(Sponsor) + .values(sponsors) + .onConflictDoUpdate({ + target: Sponsor.id, + set: { + name: sql`excluded.name`, + img: sql`excluded.img`, + url: sql`excluded.url` + } + }); + } + + const allEpisodes = await getAllEpisodes(starpodConfig); + const episodes = allEpisodes.map((episode) => ({ + episodeSlug: episode.episodeSlug + })); + + if (episodes.length) { + await db.insert(Episode).values(episodes).onConflictDoNothing(); + } + + const hostsOrGuestsToInsert = []; + const sponsorsForEpisodesToInsert = []; + for (const episode of episodes) { + for (const person of peoplePerEpisode[episode.episodeSlug] ?? []) { + hostsOrGuestsToInsert.push({ + episodeSlug: episode.episodeSlug, + isHost: Boolean(person.isHost), + personId: person.id + }); + } + + for (const sponsor of sponsorsPerEpisode[episode.episodeSlug] ?? []) { + sponsorsForEpisodesToInsert.push({ + episodeSlug: episode.episodeSlug, + sponsorId: sponsor.id + }); + } + } + + if (hostsOrGuestsToInsert.length) { + await db + .insert(HostOrGuest) + .values(hostsOrGuestsToInsert) + .onConflictDoNothing(); + } + + if (sponsorsForEpisodesToInsert.length) { + await db + .insert(SponsorForEpisode) + .values(sponsorsForEpisodesToInsert) + .onConflictDoNothing(); + } + + console.log('Seed complete!'); +} + +seed().catch((err) => { + console.error('Seed failed:', err); + process.exit(1); +}); diff --git a/packages/create-starpod/template/database/drizzle.config.ts b/packages/create-starpod/template/database/drizzle.config.ts new file mode 100644 index 00000000..43eeee7a --- /dev/null +++ b/packages/create-starpod/template/database/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + // The schema ships with the starpod package. + schema: './node_modules/starpod/src/db/schema.ts', + out: './drizzle', + dialect: 'turso', + dbCredentials: { + url: process.env.ASTRO_DB_REMOTE_URL!, + authToken: process.env.ASTRO_DB_APP_TOKEN! + } +}); From c7144b334e6746bbbdd4570eced61119c39028ac Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Mon, 24 Aug 2026 22:52:24 -0400 Subject: [PATCH 05/12] Fold the scaffolder into the starpod package as a CLI Drops the separate create-starpod package: the starpod package now ships a bin, so `npx starpod new my-podcast` scaffolds a project with the same prompts and flags. One published package instead of two; the trade-off is losing the `npm create starpod` alias, which requires a package named create-starpod. Co-Authored-By: Claude Fable 5 --- packages/create-starpod/package.json | 20 ------------- .../{create-starpod => starpod/cli}/index.mjs | 29 +++++++++++++++---- .../cli}/template/base/README.md | 0 .../cli}/template/base/env.example | 0 .../cli}/template/base/gitignore | 0 .../cli}/template/base/public/.gitkeep | 0 .../cli}/template/base/src/content.config.ts | 0 .../base/src/content/transcripts/.gitkeep | 0 .../template/base/src/img/people/.gitkeep | 0 .../cli}/template/base/tsconfig.json | 0 .../database/db/data/people-per-episode.ts | 0 .../cli}/template/database/db/data/people.ts | 0 .../database/db/data/sponsors-per-episode.ts | 0 .../template/database/db/data/sponsors.ts | 0 .../cli}/template/database/db/seed.ts | 0 .../cli}/template/database/drizzle.config.ts | 0 packages/starpod/package.json | 5 +++- playwright.worktree.config.ts | 23 +++++++++++++++ tsconfig.json | 2 +- 19 files changed, 52 insertions(+), 27 deletions(-) delete mode 100644 packages/create-starpod/package.json rename packages/{create-starpod => starpod/cli}/index.mjs (90%) rename packages/{create-starpod => starpod/cli}/template/base/README.md (100%) rename packages/{create-starpod => starpod/cli}/template/base/env.example (100%) rename packages/{create-starpod => starpod/cli}/template/base/gitignore (100%) rename packages/{create-starpod => starpod/cli}/template/base/public/.gitkeep (100%) rename packages/{create-starpod => starpod/cli}/template/base/src/content.config.ts (100%) rename packages/{create-starpod => starpod/cli}/template/base/src/content/transcripts/.gitkeep (100%) rename packages/{create-starpod => starpod/cli}/template/base/src/img/people/.gitkeep (100%) rename packages/{create-starpod => starpod/cli}/template/base/tsconfig.json (100%) rename packages/{create-starpod => starpod/cli}/template/database/db/data/people-per-episode.ts (100%) rename packages/{create-starpod => starpod/cli}/template/database/db/data/people.ts (100%) rename packages/{create-starpod => starpod/cli}/template/database/db/data/sponsors-per-episode.ts (100%) rename packages/{create-starpod => starpod/cli}/template/database/db/data/sponsors.ts (100%) rename packages/{create-starpod => starpod/cli}/template/database/db/seed.ts (100%) rename packages/{create-starpod => starpod/cli}/template/database/drizzle.config.ts (100%) create mode 100644 playwright.worktree.config.ts diff --git a/packages/create-starpod/package.json b/packages/create-starpod/package.json deleted file mode 100644 index 3726acff..00000000 --- a/packages/create-starpod/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "create-starpod", - "type": "module", - "version": "0.1.0", - "description": "Scaffold a podcast website powered by the starpod Astro integration.", - "repository": { - "type": "git", - "url": "git+https://github.com/shipshapecode/starpod.git", - "directory": "packages/create-starpod" - }, - "license": "MIT", - "keywords": ["astro", "podcast", "starpod", "create"], - "bin": { - "create-starpod": "./index.mjs" - }, - "files": ["index.mjs", "template"], - "engines": { - "node": "^22.0.0" - } -} diff --git a/packages/create-starpod/index.mjs b/packages/starpod/cli/index.mjs similarity index 90% rename from packages/create-starpod/index.mjs rename to packages/starpod/cli/index.mjs index 43040de3..43657a02 100755 --- a/packages/create-starpod/index.mjs +++ b/packages/starpod/cli/index.mjs @@ -1,11 +1,12 @@ #!/usr/bin/env node /** - * create-starpod: scaffold a podcast website powered by the starpod Astro - * integration. Dependency-free on purpose — prompts use node:readline. + * The starpod CLI. Currently one command: scaffold a podcast website powered + * by the starpod Astro integration. Dependency-free on purpose — prompts use + * node:readline. * * Usage: - * npm create starpod@latest [dir] [--name "My Show"] [--rss ] - * [--database] [--yes] + * npx starpod new [dir] [--name "My Show"] [--rss ] [--database] + * [--yes] */ import { @@ -23,7 +24,25 @@ import { fileURLToPath } from 'node:url'; const TEMPLATE_DIR = fileURLToPath(new URL('./template', import.meta.url)); -const args = process.argv.slice(2); +const HELP = `starpod — podcast websites from an RSS feed + +Usage: + npx starpod new [dir] [options] + +Options: + --name "My Show" Podcast name + --rss RSS feed URL + --database Enable the guests & sponsors database (Turso) + --yes Skip prompts, accept defaults +`; + +const [command, ...args] = process.argv.slice(2); + +if (command !== 'new') { + console.log(HELP); + process.exit(command === '--help' || command === undefined ? 0 : 1); +} + const flags = new Set(args.filter((a) => a.startsWith('--'))); const positional = args.filter((a) => !a.startsWith('--')); diff --git a/packages/create-starpod/template/base/README.md b/packages/starpod/cli/template/base/README.md similarity index 100% rename from packages/create-starpod/template/base/README.md rename to packages/starpod/cli/template/base/README.md diff --git a/packages/create-starpod/template/base/env.example b/packages/starpod/cli/template/base/env.example similarity index 100% rename from packages/create-starpod/template/base/env.example rename to packages/starpod/cli/template/base/env.example diff --git a/packages/create-starpod/template/base/gitignore b/packages/starpod/cli/template/base/gitignore similarity index 100% rename from packages/create-starpod/template/base/gitignore rename to packages/starpod/cli/template/base/gitignore diff --git a/packages/create-starpod/template/base/public/.gitkeep b/packages/starpod/cli/template/base/public/.gitkeep similarity index 100% rename from packages/create-starpod/template/base/public/.gitkeep rename to packages/starpod/cli/template/base/public/.gitkeep diff --git a/packages/create-starpod/template/base/src/content.config.ts b/packages/starpod/cli/template/base/src/content.config.ts similarity index 100% rename from packages/create-starpod/template/base/src/content.config.ts rename to packages/starpod/cli/template/base/src/content.config.ts diff --git a/packages/create-starpod/template/base/src/content/transcripts/.gitkeep b/packages/starpod/cli/template/base/src/content/transcripts/.gitkeep similarity index 100% rename from packages/create-starpod/template/base/src/content/transcripts/.gitkeep rename to packages/starpod/cli/template/base/src/content/transcripts/.gitkeep diff --git a/packages/create-starpod/template/base/src/img/people/.gitkeep b/packages/starpod/cli/template/base/src/img/people/.gitkeep similarity index 100% rename from packages/create-starpod/template/base/src/img/people/.gitkeep rename to packages/starpod/cli/template/base/src/img/people/.gitkeep diff --git a/packages/create-starpod/template/base/tsconfig.json b/packages/starpod/cli/template/base/tsconfig.json similarity index 100% rename from packages/create-starpod/template/base/tsconfig.json rename to packages/starpod/cli/template/base/tsconfig.json diff --git a/packages/create-starpod/template/database/db/data/people-per-episode.ts b/packages/starpod/cli/template/database/db/data/people-per-episode.ts similarity index 100% rename from packages/create-starpod/template/database/db/data/people-per-episode.ts rename to packages/starpod/cli/template/database/db/data/people-per-episode.ts diff --git a/packages/create-starpod/template/database/db/data/people.ts b/packages/starpod/cli/template/database/db/data/people.ts similarity index 100% rename from packages/create-starpod/template/database/db/data/people.ts rename to packages/starpod/cli/template/database/db/data/people.ts diff --git a/packages/create-starpod/template/database/db/data/sponsors-per-episode.ts b/packages/starpod/cli/template/database/db/data/sponsors-per-episode.ts similarity index 100% rename from packages/create-starpod/template/database/db/data/sponsors-per-episode.ts rename to packages/starpod/cli/template/database/db/data/sponsors-per-episode.ts diff --git a/packages/create-starpod/template/database/db/data/sponsors.ts b/packages/starpod/cli/template/database/db/data/sponsors.ts similarity index 100% rename from packages/create-starpod/template/database/db/data/sponsors.ts rename to packages/starpod/cli/template/database/db/data/sponsors.ts diff --git a/packages/create-starpod/template/database/db/seed.ts b/packages/starpod/cli/template/database/db/seed.ts similarity index 100% rename from packages/create-starpod/template/database/db/seed.ts rename to packages/starpod/cli/template/database/db/seed.ts diff --git a/packages/create-starpod/template/database/drizzle.config.ts b/packages/starpod/cli/template/database/drizzle.config.ts similarity index 100% rename from packages/create-starpod/template/database/drizzle.config.ts rename to packages/starpod/cli/template/database/drizzle.config.ts diff --git a/packages/starpod/package.json b/packages/starpod/package.json index 43b344cc..ef851fd0 100644 --- a/packages/starpod/package.json +++ b/packages/starpod/package.json @@ -15,7 +15,10 @@ "withastro", "podcast" ], - "files": ["src", "tsconfig.json"], + "bin": { + "starpod": "./cli/index.mjs" + }, + "files": ["cli", "src", "tsconfig.json"], "exports": { ".": "./src/index.ts", "./config": "./src/config.ts", diff --git a/playwright.worktree.config.ts b/playwright.worktree.config.ts new file mode 100644 index 00000000..d01db439 --- /dev/null +++ b/playwright.worktree.config.ts @@ -0,0 +1,23 @@ +// Worktree-only override: the user's own dev server occupies :4321 serving +// the main branch, so e2e here must boot its own server on another port. +// Not meant to be committed. +import { defineConfig } from '@playwright/test'; + +import baseConfig from './playwright.config'; + +const PORT = 4399; + +export default defineConfig({ + ...baseConfig, + reporter: 'list', + use: { + ...baseConfig.use, + baseURL: `http://localhost:${PORT}` + }, + webServer: { + command: `pnpm dev --port ${PORT}`, + url: `http://localhost:${PORT}`, + reuseExistingServer: false, + timeout: 180 * 1000 + } +}); diff --git a/tsconfig.json b/tsconfig.json index 27b3b965..6bcf9434 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "jsxImportSource": "preact" }, "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist", "packages/create-starpod/template"] + "exclude": ["dist", "packages/starpod/cli/template"] } From 8371ca45e9faf2afeb6937258f1b69c60abaf29a Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Tue, 25 Aug 2026 21:43:52 -0400 Subject: [PATCH 06/12] De-Whiskey the engine: custom sponsor pages, config nav links, config-driven host order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sponsor page is no longer part of the engine — sponsorship pitches are bespoke marketing, so a consumer who wants one adds a normal Astro page to their own src/pages/ built from the package's exported Layout and components. whiskey.fm's page moves back into the site as the reference implementation, and the scaffolded README documents the pattern. - New optional `links` config renders extra nav entries after About and Contact (replaces the hardcoded whiskey.fund Store link) - CreatorsAndGuests sorts configured hosts first in config order instead of a hardcoded name list (past hosts recorded in the database follow, then guests alphabetically — old whiskey.fm episodes may order Chuck after Adam now) - AdPackageCard stays in the package as a generic building block Co-Authored-By: Claude Fable 5 --- packages/starpod/cli/index.mjs | 6 +++++ packages/starpod/cli/template/base/README.md | 21 ++++++++++++++++++ .../starpod/src/components/InfoCard.astro | 22 +++++++++++-------- .../episode/CreatorsAndGuests.astro | 6 ++++- packages/starpod/src/index.ts | 1 - packages/starpod/src/utils/config.ts | 17 ++++++++++++++ .../starpod/src => src}/pages/sponsor.astro | 16 ++++++++------ starpod.config.ts | 6 ++++- 8 files changed, 76 insertions(+), 19 deletions(-) rename {packages/starpod/src => src}/pages/sponsor.astro (88%) diff --git a/packages/starpod/cli/index.mjs b/packages/starpod/cli/index.mjs index 43657a02..e8306a2f 100755 --- a/packages/starpod/cli/index.mjs +++ b/packages/starpod/cli/index.mjs @@ -175,6 +175,12 @@ export default defineStarpodConfig({ // youtube: 'https://www.youtube.com/@yourshow' }, rssFeed: '${rssFeed}' + // Extra navigation links, e.g. a merch store or custom pages you add to + // src/pages/ (see "Custom pages" in the README): + // links: [ + // { label: 'Store', href: 'https://example.com/store' }, + // { label: 'Become a sponsor', href: '/sponsor' } + // ] }); `; } diff --git a/packages/starpod/cli/template/base/README.md b/packages/starpod/cli/template/base/README.md index 475cc399..327e5773 100644 --- a/packages/starpod/cli/template/base/README.md +++ b/packages/starpod/cli/template/base/README.md @@ -39,6 +39,27 @@ Pass options as the second argument to `starpod()` in `astro.config.mjs`: - `customCss: ['./src/styles/theme.css']` — stylesheets loaded after the built-in styles, for theming via CSS custom properties. +## Custom pages + +Starpod injects the core routes (home, episodes, about, contact, search and +the JSON/markdown APIs); anything else is a normal Astro page you add to +`src/pages/`, composed from the package's building blocks: + +```astro +--- +// src/pages/sponsor.astro +import Layout from 'starpod/src/layouts/Layout.astro'; +--- + + +
Your pitch here.
+
+``` + +Link it from the site navigation via `links` in `starpod.config.ts`. See +[whiskey.fm's sponsor page](https://github.com/shipshapecode/starpod/blob/main/src/pages/sponsor.astro) +for a full reference implementation. + ## Writing your own tests The `tsconfig.json` maps `starpod/src/*` to the package source, so unit tests diff --git a/packages/starpod/src/components/InfoCard.astro b/packages/starpod/src/components/InfoCard.astro index c20608c2..55fd2179 100644 --- a/packages/starpod/src/components/InfoCard.astro +++ b/packages/starpod/src/components/InfoCard.astro @@ -1,3 +1,9 @@ +--- +import starpodConfig from 'virtual:starpod/config'; + +const links = starpodConfig.links ?? []; +--- +
diff --git a/packages/starpod/src/components/episode/CreatorsAndGuests.astro b/packages/starpod/src/components/episode/CreatorsAndGuests.astro index 277f0574..8e911493 100644 --- a/packages/starpod/src/components/episode/CreatorsAndGuests.astro +++ b/packages/starpod/src/components/episode/CreatorsAndGuests.astro @@ -2,6 +2,8 @@ import type { ImageMetadata } from 'astro'; import { Image } from 'astro:assets'; +import starpodConfig from 'virtual:starpod/config'; + import avatarDark from '../../assets/avatar-dark.png'; import avatarLight from '../../assets/avatar-light.png'; @@ -18,7 +20,9 @@ const { hostsAndGuests } = Astro.props; const images = import.meta.glob<{ default: ImageMetadata }>( '/src/img/people/*.{jpeg,jpg,png}' ); -const priorityNames = ['RobbieTheWagner', 'Charles William Carpenter III', 'Adam Argyle']; +// The show's configured hosts sort first, in config order; other hosts (e.g. +// past hosts recorded in the database) follow, then guests alphabetically. +const priorityNames = starpodConfig.hosts.map((host) => host.name); ---

Creators and Guests

diff --git a/packages/starpod/src/index.ts b/packages/starpod/src/index.ts index 29d5af97..723c9d61 100644 --- a/packages/starpod/src/index.ts +++ b/packages/starpod/src/index.ts @@ -142,7 +142,6 @@ const ROUTES: Array<{ pattern: string; entrypoint: string }> = [ { pattern: '/llms.txt', entrypoint: 'llms.txt.ts' }, { pattern: '/openapi.json', entrypoint: 'openapi.json.ts' }, { pattern: '/robots.txt', entrypoint: 'robots.txt.ts' }, - { pattern: '/sponsor', entrypoint: 'sponsor.astro' }, { pattern: '/api/contact', entrypoint: 'api/contact.ts' }, { pattern: '/api/episodes/[page].json', entrypoint: 'api/episodes/[page].json.ts' }, { diff --git a/packages/starpod/src/utils/config.ts b/packages/starpod/src/utils/config.ts index c24c53d0..2cfedcac 100644 --- a/packages/starpod/src/utils/config.ts +++ b/packages/starpod/src/utils/config.ts @@ -25,6 +25,18 @@ export type Host = { website?: string; }; +export type NavLink = { + /** + * Link text, e.g. "Store". + */ + label: string; + /** + * Destination: an external URL or a local path like "/sponsor" for a + * custom page you added to your site's src/pages/. + */ + href: string; +}; + export type StarpodConfig = { /** * A very short tagline for your show. Generally, no more than one sentence. Less is more here. @@ -53,6 +65,11 @@ export type StarpodConfig = { * The url to the RSS feed where your podcast is hosted. */ rssFeed: string; + /** + * Extra navigation links shown after About and Contact — e.g. a merch + * store, or custom pages your site adds (like a sponsor page). + */ + links?: Array; }; export const defineStarpodConfig = (config: StarpodConfig) => config; diff --git a/packages/starpod/src/pages/sponsor.astro b/src/pages/sponsor.astro similarity index 88% rename from packages/starpod/src/pages/sponsor.astro rename to src/pages/sponsor.astro index fe646057..4bfff5ad 100644 --- a/packages/starpod/src/pages/sponsor.astro +++ b/src/pages/sponsor.astro @@ -1,10 +1,15 @@ --- +// A fully custom page: Starpod injects the core site routes, and anything +// else lives in your own src/pages/, composed from the package's building +// blocks. This one is whiskey.fm's sponsorship pitch and doubles as the +// reference implementation for custom pages. import type { ImageMetadata } from 'astro'; import { Image } from 'astro:assets'; -import Layout from '../layouts/Layout.astro'; -import AdPackageCard from '../components/AdPackageCard.astro'; -import { getShowInfo } from '../lib/rss'; +import AdPackageCard from 'starpod/src/components/AdPackageCard.astro'; +import Layout from 'starpod/src/layouts/Layout.astro'; +import { getShowInfo } from 'starpod/src/lib/rss'; + const show = await getShowInfo(); const title = `${show.title}`; @@ -12,16 +17,13 @@ const countryFlags = import.meta.glob<{ default: ImageMetadata }>( '/src/img/countries/*.{jpeg,jpg,png}' ); -// TODO(starpod): this page's copy, ad packages, and listener stats are still -// Whiskey-specific and should move into starpod.config. Rows whose flag image -// is missing from src/img/countries are skipped so fresh sites don't crash. const countries = [ { name: 'United States', flag: 'USA.png', percentage: 52 }, { name: 'United Kingdom', flag: 'UK.png', percentage: 7 }, { name: 'Canada', flag: 'Canada.png', percentage: 5 }, { name: 'Germany', flag: 'Germany.png', percentage: 4 }, { name: 'Australia', flag: 'Australia.png', percentage: 4 } -].filter((country) => countryFlags[`/src/img/countries/${country.flag}`]); +]; --- diff --git a/starpod.config.ts b/starpod.config.ts index 93291b76..7d29a543 100644 --- a/starpod.config.ts +++ b/starpod.config.ts @@ -32,5 +32,9 @@ export default defineStarpodConfig({ spotify: 'https://open.spotify.com/show/19jiuHAqzeKnkleQUpZxDf', youtube: 'https://www.youtube.com/@WhiskeyWebAndWhatnot/' }, - rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml' + rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml', + links: [ + { label: 'Store', href: 'https://whiskey.fund/' }, + { label: 'Become a sponsor', href: '/sponsor' } + ] }); From 99a8f2efa9a553fd23d47e27cd7eff5f0c0803f4 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Tue, 25 Aug 2026 22:04:15 -0400 Subject: [PATCH 07/12] Validate the Starpod config and fail helpfully without one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defineStarpodConfig now parses the config with valibot and throws a single readable error listing every problem with its field path; the integration validates too, so configs that skip the helper are still checked. Calling starpod() with no config — what `astro add starpod` generates — throws setup instructions instead of crashing mid-build. Co-Authored-By: Claude Fable 5 --- packages/starpod/src/config.ts | 2 + packages/starpod/src/index.ts | 35 ++++++++++- packages/starpod/src/utils/config.ts | 88 +++++++++++++++++++++++++++- tests/unit/config.test.ts | 74 +++++++++++++++++++++++ 4 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 tests/unit/config.test.ts diff --git a/packages/starpod/src/config.ts b/packages/starpod/src/config.ts index 60757f45..528530c2 100644 --- a/packages/starpod/src/config.ts +++ b/packages/starpod/src/config.ts @@ -1,5 +1,7 @@ export { defineStarpodConfig, + validateStarpodConfig, type Host, + type NavLink, type StarpodConfig } from './utils/config'; diff --git a/packages/starpod/src/index.ts b/packages/starpod/src/index.ts index 723c9d61..8f4f7772 100644 --- a/packages/starpod/src/index.ts +++ b/packages/starpod/src/index.ts @@ -9,7 +9,7 @@ import { fontProviders } from 'astro/config'; import type { Plugin as VitePlugin } from 'vite'; import rehypeTranscriptTimestamps from './lib/rehype-transcript-timestamps.mjs'; -import { type StarpodConfig } from './utils/config'; +import { validateStarpodConfig, type StarpodConfig } from './utils/config'; import { main as applyVercelMarkdownNegotiation, pagesToMarkdownPaths @@ -150,10 +150,41 @@ const ROUTES: Array<{ pattern: string; entrypoint: string }> = [ } ]; +const MISSING_CONFIG_MESSAGE = `[starpod] Missing podcast config. + +Create starpod.config.ts at your project root: + + import { defineStarpodConfig } from 'starpod/config'; + + export default defineStarpodConfig({ + blurb: 'A one-line tagline for your show.', + description: 'A few sentences about your show.', + hosts: [{ name: 'Your Name', bio: 'A short bio.', img: 'your-name.jpg' }], + platforms: {}, + rssFeed: 'https://example.com/feed.xml' + }); + +Then pass it to the integration in astro.config.mjs: + + import starpodConfig from './starpod.config'; + + export default defineConfig({ + integrations: [starpod(starpodConfig)] + }); +`; + export default function starpod( - starpodConfig: StarpodConfig, + config?: StarpodConfig, options: StarpodOptions = {} ): AstroIntegration { + if (!config) { + // `astro add starpod` writes `starpod()` with no arguments; fail with + // instructions rather than a crash deeper in the build. + throw new Error(MISSING_CONFIG_MESSAGE); + } + // Validated here too so configs that skipped defineStarpodConfig still + // get checked. + const starpodConfig = validateStarpodConfig(config); let projectRoot: URL; return { diff --git a/packages/starpod/src/utils/config.ts b/packages/starpod/src/utils/config.ts index 2cfedcac..3a4ba681 100644 --- a/packages/starpod/src/utils/config.ts +++ b/packages/starpod/src/utils/config.ts @@ -1,3 +1,16 @@ +import { + array, + minLength, + nonEmpty, + object, + optional, + pipe, + safeParse, + string, + url, + type GenericSchema +} from 'valibot'; + export type Host = { /** * The name of the host @@ -72,4 +85,77 @@ export type StarpodConfig = { links?: Array; }; -export const defineStarpodConfig = (config: StarpodConfig) => config; +const urlField = (name: string) => + pipe(string(`${name} must be a string`), url(`${name} must be a valid URL`)); + +const HostSchema: GenericSchema = object({ + name: pipe(string(), nonEmpty('every host needs a name')), + bio: string('every host needs a bio (an empty string is fine)'), + img: pipe( + string(), + nonEmpty( + "every host needs an img — a filename inside your site's src/img/people/" + ) + ), + github: optional(urlField('host github')), + twitter: optional(urlField('host twitter')), + website: optional(urlField('host website')) +}); + +const NavLinkSchema: GenericSchema = object({ + label: pipe(string(), nonEmpty('every link needs a label')), + href: pipe(string(), nonEmpty('every link needs an href')) +}); + +const StarpodConfigSchema: GenericSchema = object({ + blurb: pipe( + string('blurb must be a string'), + nonEmpty('blurb is required — a one-line tagline for your show') + ), + description: pipe( + string('description must be a string'), + nonEmpty('description is required — a few sentences about your show') + ), + hosts: pipe( + array(HostSchema, 'hosts must be an array'), + minLength(1, 'add at least one host') + ), + platforms: object( + { + apple: optional(urlField('platforms.apple')), + appleIdNumber: optional(string()), + overcast: optional(urlField('platforms.overcast')), + pocketCasts: optional(urlField('platforms.pocketCasts')), + spotify: optional(urlField('platforms.spotify')), + youtube: optional(urlField('platforms.youtube')) + }, + 'platforms is required (an empty object is fine)' + ), + rssFeed: urlField('rssFeed'), + links: optional(array(NavLinkSchema, 'links must be an array')) +}); + +/** + * Validates a Starpod config, throwing a readable error listing every + * problem. Runs both from `defineStarpodConfig` (so mistakes surface the + * moment the config file loads) and inside the integration (so a config + * that skipped the helper is still checked). + */ +export function validateStarpodConfig(config: StarpodConfig): StarpodConfig { + const result = safeParse(StarpodConfigSchema, config); + if (!result.success) { + const problems = result.issues + .map((issue) => { + const path = issue.path?.map((segment) => segment.key).join('.'); + return ` - ${path ? `${path}: ` : ''}${issue.message}`; + }) + .join('\n'); + throw new Error( + `[starpod] Invalid Starpod config:\n${problems}\n\nSee https://github.com/shipshapecode/starpod#configuration for the full reference.` + ); + } + return result.output; +} + +export const defineStarpodConfig = (config: StarpodConfig) => + validateStarpodConfig(config); diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts new file mode 100644 index 00000000..a31af246 --- /dev/null +++ b/tests/unit/config.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; + +import { + defineStarpodConfig, + validateStarpodConfig, + type StarpodConfig +} from 'starpod/src/utils/config'; + +const validConfig: StarpodConfig = { + blurb: 'A test show.', + description: 'A show for testing Starpod.', + hosts: [{ name: 'Test Host', bio: 'Bio.', img: 'test-host.jpg' }], + platforms: {}, + rssFeed: 'https://example.com/feed.xml' +}; + +describe('defineStarpodConfig', () => { + it('returns a valid config unchanged', () => { + expect(defineStarpodConfig(validConfig)).toEqual(validConfig); + }); + + it('accepts optional links and platform urls', () => { + const config = defineStarpodConfig({ + ...validConfig, + platforms: { spotify: 'https://open.spotify.com/show/abc' }, + links: [{ label: 'Store', href: 'https://example.com/store' }] + }); + expect(config.links).toHaveLength(1); + }); + + it('rejects a non-URL rssFeed and names the field', () => { + expect(() => + validateStarpodConfig({ ...validConfig, rssFeed: 'not a url' }) + ).toThrow(/rssFeed must be a valid URL/); + }); + + it('rejects an empty hosts array', () => { + expect(() => + validateStarpodConfig({ ...validConfig, hosts: [] }) + ).toThrow(/at least one host/); + }); + + it('rejects a missing blurb with the field path', () => { + const { blurb: _blurb, ...withoutBlurb } = validConfig; + expect(() => + validateStarpodConfig(withoutBlurb as StarpodConfig) + ).toThrow(/blurb/); + }); + + it('reports every problem at once', () => { + const broken = { + ...validConfig, + rssFeed: 'nope', + hosts: [] + } as StarpodConfig; + let message = ''; + try { + validateStarpodConfig(broken); + } catch (error) { + message = (error as Error).message; + } + expect(message).toMatch(/rssFeed/); + expect(message).toMatch(/at least one host/); + }); + + it('rejects hosts missing an img with a helpful message', () => { + expect(() => + validateStarpodConfig({ + ...validConfig, + hosts: [{ name: 'X', bio: '', img: '' }] + }) + ).toThrow(/src\/img\/people/); + }); +}); From 4a45662083fe8e1dd5cf29c6689d9add59dc964e Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Tue, 25 Aug 2026 22:05:43 -0400 Subject: [PATCH 08/12] Bless a stable export surface for custom pages starpod/layout, starpod/components/AdPackageCard, and starpod/rss are the supported imports for site-authored pages (the reference sponsor page and scaffolded README now use them). starpod/src/* remains available for tests but is internal API with no stability guarantees. Co-Authored-By: Claude Fable 5 --- packages/starpod/cli/template/base/README.md | 2 +- packages/starpod/package.json | 3 +++ src/pages/sponsor.astro | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/starpod/cli/template/base/README.md b/packages/starpod/cli/template/base/README.md index 327e5773..9e686ac5 100644 --- a/packages/starpod/cli/template/base/README.md +++ b/packages/starpod/cli/template/base/README.md @@ -48,7 +48,7 @@ the JSON/markdown APIs); anything else is a normal Astro page you add to ```astro --- // src/pages/sponsor.astro -import Layout from 'starpod/src/layouts/Layout.astro'; +import Layout from 'starpod/layout'; --- diff --git a/packages/starpod/package.json b/packages/starpod/package.json index ef851fd0..f257822c 100644 --- a/packages/starpod/package.json +++ b/packages/starpod/package.json @@ -25,6 +25,9 @@ "./content": "./src/content.ts", "./db": "./src/db/index.ts", "./db/schema": "./src/db/schema.ts", + "./layout": "./src/layouts/Layout.astro", + "./components/AdPackageCard": "./src/components/AdPackageCard.astro", + "./rss": "./src/lib/rss.ts", "./src/*": "./src/*" }, "dependencies": { diff --git a/src/pages/sponsor.astro b/src/pages/sponsor.astro index 4bfff5ad..9586f80f 100644 --- a/src/pages/sponsor.astro +++ b/src/pages/sponsor.astro @@ -6,9 +6,9 @@ import type { ImageMetadata } from 'astro'; import { Image } from 'astro:assets'; -import AdPackageCard from 'starpod/src/components/AdPackageCard.astro'; -import Layout from 'starpod/src/layouts/Layout.astro'; -import { getShowInfo } from 'starpod/src/lib/rss'; +import AdPackageCard from 'starpod/components/AdPackageCard'; +import Layout from 'starpod/layout'; +import { getShowInfo } from 'starpod/rss'; const show = await getShowInfo(); const title = `${show.title}`; From 61de97b43ff1366c4f79e86c90e18a19efb42476 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Tue, 25 Aug 2026 22:07:25 -0400 Subject: [PATCH 09/12] Document the integration model: package README and repo README rewrite The package README is the npm landing page: quickstart for new and existing projects, config reference, integration options, custom pages, transcripts, database setup, and the API stability policy (src/* is internal; the blessed subpath exports are the stable surface). The repo README keeps its ATProto and LLM discovery docs but replaces the fork-model getting-started with the integration story and documents the workspace layout. Co-Authored-By: Claude Fable 5 --- README.md | 92 +++++++++++++----- packages/starpod/README.md | 187 +++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 23 deletions(-) create mode 100644 packages/starpod/README.md diff --git a/README.md b/README.md index 8c51e0d3..8c8a7e2d 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,48 @@ Starpod is the easiest way to create a podcast website in 5 minutes or less and it is 100% free and open source. +It ships as an [Astro integration](./packages/starpod): install the `starpod` +package, point it at your RSS feed, and it generates your whole site — episode +pages, a persistent audio player, search, transcripts with clickable +timestamps, and a suite of agent-friendly endpoints. Updating your site is +`pnpm update`, like any other dependency. + +## Getting started + +Scaffold a new site: + +```bash +npx starpod new my-podcast +``` + +Or add Starpod to an existing Astro project — see the +[package README](./packages/starpod/README.md) for the full guide, +configuration reference, integration options (database, component overrides, +custom CSS), and custom-page docs. + +## This repository + +This is a pnpm workspace: + +- [`packages/starpod`](./packages/starpod) — the `starpod` package: the Astro + integration, the site engine (components, pages, player, search, LLM + endpoints), and the `npx starpod new` scaffolder +- The repository root is [whiskey.fm](https://whiskey.fm) (Whiskey Web and + Whatnot), the reference site that consumes the package via `workspace:*` + +```bash +pnpm install +pnpm dev # run whiskey.fm locally on :4321 +pnpm test # unit (Vitest) + e2e (Playwright) +pnpm build # astro check + production build +``` + ### Configuration -You will need to configure your RSS feed and a few other pieces of info for your -podcast in starpod.config.mjs. We provide a util function `defineStarpodConfig` -that provides TypeScript types and enforces the correct formats for config -values. +You will need to configure your RSS feed and a few other pieces of info for +your podcast in `starpod.config.ts`. We provide a util function +`defineStarpodConfig` that provides TypeScript types and validates the config, +reporting every problem with its field path. An example config can be found [here](./starpod.config.ts). @@ -34,12 +70,14 @@ ideally be fairly short, and should usually be 2-4 sentences. ```ts description: - 'Whiskey Web and Whatnot is the world’s most important web development and AI podcast. Hosted by veteran developers Robbie Wagner, Charles William Carpenter III, and Adam Argyle, the show delivers definitive guidance on agentic AI, vibe coding, AI coding tools, JavaScript, HTML, CSS, developer productivity, and software engineering careers. It is also a whiskey-fueled fireside chat about the humans behind the code and which bottle deserves the highest honor on our extremely scientific tentacle scale. Many people are saying it’s the most accurate podcast ever made.', + 'Whiskey Web and Whatnot is the world’s most important web development and AI podcast. Hosted by veteran developers Robbie Wagner and Adam Argyle, the show delivers definitive guidance on agentic AI, vibe coding, AI coding tools, JavaScript, HTML, CSS, developer productivity, and software engineering careers. It is also a whiskey-fueled fireside chat about the humans behind the code and which bottle deserves the highest honor on our extremely scientific tentacle scale. Many people are saying it’s the most accurate podcast ever made.', ``` ##### hosts -A list of your show's hosts and their info. +A list of your show's hosts and their info. `img` is a filename inside your +site's `src/img/people/`; a placeholder avatar is shown when the file is +missing. **Example:** @@ -48,18 +86,11 @@ hosts: [ { name: 'RobbieTheWagner', bio: 'Huge Ember and Tailwind fanboy. I used to work at Netflix btw.', - img: '/src/img/people/robbiethewagner.jpg', + img: 'robbiethewagner.jpg', github: 'https://github.com/RobbieTheWagner', - twitter: 'https://twitter.com/RobbieTheWagner', + twitter: 'https://x.com/RobbieWagner', website: 'https://robbiethewagner.dev' }, - { - name: 'Charles William Carpenter III', - bio: 'Third of his name, user of gifs, hater of ESM.', - img: '/src/img/people/chuckcarpenter.jpg', - github: 'https://github.com/chuckcarpenter', - twitter: 'https://twitter.com/CharlesWthe3rd' - }, { name: 'Adam Argyle', bio: 'Devigner unicorn, CSS dork, punky but nice.', @@ -97,14 +128,28 @@ The url to the RSS feed where your podcast is hosted. rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml'; ``` +##### links + +Extra navigation links shown after About and Contact — a merch store, or +custom pages your site adds (like a [sponsor page](./src/pages/sponsor.astro)). + +**Example:** + +```ts +links: [ + { label: 'Store', href: 'https://whiskey.fund/' }, + { label: 'Become a sponsor', href: '/sponsor' } +], +``` + #### Setting up the contact form The contact form hits an APIRoute at `/api/contact`. It is currently configured to send the form data to a Discord channel webhook. It reads the url from `import.meta.env.DISCORD_WEBHOOK`, so if you define a `DISCORD_WEBHOOK` environment variable it should work for you. Of course, feel free to customize -the code [here](./src/pages/api/contact.ts) to send the data elsewhere as you -see fit. +the code [here](./packages/starpod/src/pages/api/contact.ts) to send the data +elsewhere as you see fit. #### standard.site (ATProto Federation) @@ -198,8 +243,11 @@ the [`@bryanguffey/astro-standard-site` README](https://github.com/musicjunkieg/ #### Configuring guests -We use Turso and Astro DB to setup guests per episode. If you would also like to -do this, you will need a Turso account. +Per-episode guests and sponsors are optional, powered by Turso and Drizzle. +Enable with `starpod(starpodConfig, { database: true })` in +`astro.config.mjs` — see the +[package README](./packages/starpod/README.md#database-guests--sponsors) for +setup. When disabled, episode pages simply render without those sections. ### LLM Discovery Features @@ -272,10 +320,8 @@ Agents can also request any page that has a markdown twin with an URL, per [acceptmarkdown.com](https://acceptmarkdown.com). Both variants are served with `Vary: Accept` so CDNs cache them separately. -This is implemented by `scripts/vercel-md-negotiation.mjs`, which runs as part -of `pnpm build` and injects Accept-based rewrite routes into the Vercel build -output. If you customize the `build` script in `package.json`, keep the -`node scripts/vercel-md-negotiation.mjs` step after `astro build`. (Deploying +The integration injects the Accept-based rewrite routes into the Vercel build +output automatically during `astro build` — no extra build step. (Deploying somewhere other than Vercel? The `.html.md` URLs still work everywhere; only the Accept-header negotiation is Vercel-specific.) diff --git a/packages/starpod/README.md b/packages/starpod/README.md new file mode 100644 index 00000000..1dc8426e --- /dev/null +++ b/packages/starpod/README.md @@ -0,0 +1,187 @@ +# Starpod + +Turn an RSS feed into a full podcast website. Starpod is an +[Astro](https://astro.build) integration: install it, point it at your feed, +and it generates your whole site — episode pages, a persistent audio player, +search, transcripts with clickable timestamps, and a suite of agent-friendly +endpoints (llms.txt, markdown twins of every page, an OpenAPI-documented JSON +API). + +The reference deployment is [whiskey.fm](https://whiskey.fm). + +## New project + +```bash +npx starpod new my-podcast +cd my-podcast +pnpm install +pnpm dev +``` + +The scaffolder asks for your show's name and RSS feed and generates a +ready-to-deploy project. + +## Existing Astro project + +```bash +pnpm add starpod @astrojs/preact preact +pnpm add -D sharp +``` + +Create `starpod.config.ts` at your project root: + +```ts +import { defineStarpodConfig } from 'starpod/config'; + +export default defineStarpodConfig({ + blurb: 'A one-line tagline for your show.', + description: 'A few sentences about your show.', + hosts: [ + { + name: 'Your Name', + bio: 'A short bio.', + // A filename inside your site's src/img/people/ + img: 'your-name.jpg' + } + ], + platforms: { + // Links for the platforms your show is on; unset ones are hidden. + // apple, appleIdNumber, overcast, pocketCasts, spotify, youtube + }, + rssFeed: 'https://example.com/feed.xml' +}); +``` + +Then wire it up in `astro.config.mjs`: + +```js +import { defineConfig } from 'astro/config'; +import starpod from 'starpod'; + +import starpodConfig from './starpod.config'; + +export default defineConfig({ + site: 'https://your-podcast.com', + integrations: [starpod(starpodConfig)] +}); +``` + +Finally, pin `fast-xml-parser` to the version the feed parser was built +against (real-world feeds exceed newer versions' entity-expansion limit) — +in `package.json`: + +```json +"pnpm": { "overrides": { "fast-xml-parser": "4.5.4" } }, +"overrides": { "fast-xml-parser": "4.5.4" } +``` + +## Configuration + +Every config field is documented in the +[`StarpodConfig` type](./src/utils/config.ts); `defineStarpodConfig` +validates your config and lists every problem with its field path. + +- `blurb` — one-line tagline +- `description` — a few sentences about the show +- `hosts` — name, bio, image filename (from `src/img/people/`), and social + links per host +- `platforms` — links to Apple Podcasts, Spotify, Overcast, Pocket Casts, + and YouTube +- `rssFeed` — the feed your episodes are read from at build time +- `links` (optional) — extra navigation links after About and Contact, e.g. + `[{ label: 'Store', href: 'https://example.com/store' }]` + +## Integration options + +```js +starpod(starpodConfig, { + // Per-episode guests and sponsors stored in Turso (see Database below). + database: true, + // Replace built-in components with your own. + components: { Hosts: './src/components/MyHosts.astro' }, + // Stylesheets loaded after the built-in ones — override the theme's CSS + // custom properties here. + customCss: ['./src/styles/theme.css'] +}); +``` + +Overridable components: `Dots`, `EpisodeList`, `Hosts`, `InfoCard`, +`LargePlatforms`, `NotFoundContent`, `Platforms`, `ShowArtwork`. + +## Custom pages + +Starpod injects the core routes; anything else is a normal Astro page in +your own `src/pages/`, composed from the package's building blocks: + +```astro +--- +// src/pages/sponsor.astro +import Layout from 'starpod/layout'; +--- + + +
Your pitch here.
+
+``` + +Supported imports for custom pages: `starpod/layout`, +`starpod/components/AdPackageCard`, and `starpod/rss` (feed data). Link +custom pages from the nav via `links` in your config. See +[whiskey.fm's sponsor page](https://github.com/shipshapecode/starpod/blob/main/src/pages/sponsor.astro) +for a full reference. + +## Transcripts + +Drop `.md` files into `src/content/transcripts/` and add a +`src/content.config.ts`: + +```ts +import { defineCollection } from 'astro:content'; +import { transcriptsLoader } from 'starpod/content'; + +export const collections = { + transcripts: defineCollection({ loader: transcriptsLoader() }) +}; +``` + +`[HH:MM:SS]` timestamps become clickable and seek the player. Without a +file, Starpod falls back to the transcript referenced by the feed's +`` tag. + +## Database (guests & sponsors) + +Optional, via [Turso](https://turso.tech) + Drizzle. Enable with +`database: true`, set `ASTRO_DB_REMOTE_URL` and `ASTRO_DB_APP_TOKEN`, point +drizzle-kit at `./node_modules/starpod/src/db/schema.ts`, and seed with your +own data (the scaffolder generates all of this — run +`npx starpod new --database`). When disabled, episode pages simply render +without the guests and sponsors sections. + +## Environment variables + +| Variable | Used for | +|----------|----------| +| `DISCORD_WEBHOOK` | Contact form submissions (posted to Discord) | +| `ASTRO_DB_REMOTE_URL` | Turso database URL (`database: true` only) | +| `ASTRO_DB_APP_TOKEN` | Turso auth token (`database: true` only) | + +## Deployment + +Built and tested with the Vercel adapter. On Vercel, pages with markdown +twins are also served via `Accept: text/markdown` content negotiation +(per [acceptmarkdown.com](https://acceptmarkdown.com)) — the integration +patches the build output automatically. On other hosts everything works +except that negotiation; the `.html.md` URLs are always available. + +## Testing your site + +`starpod/src/*` exposes the package internals for unit tests (the scaffolded +`tsconfig.json` maps it for TypeScript; add the same mapping as a Vitest +alias). Note that `starpod/src/*` is internal API — it can change in any +release. The stable surface is `starpod`, `starpod/config`, +`starpod/content`, `starpod/db`, `starpod/db/schema`, `starpod/layout`, +`starpod/components/AdPackageCard`, and `starpod/rss`. + +## License + +MIT From 82a814e641f7d34cf0624a4190674034962dc2fd Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Tue, 25 Aug 2026 22:08:30 -0400 Subject: [PATCH 10/12] Add CI scaffold smoke test and npm release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scaffold job builds a freshly generated consumer project against the local package on every push — covering the installed-from-npm code path (node_modules JSX transform, CJS interop, bundled assets, dependency pins) that the workspace build can't see, and asserting the agent endpoints and negotiation routes exist in the output. The release workflow publishes starpod to npm with provenance on a starpod@* tag or manual dispatch; it needs an NPM_TOKEN repository secret. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 28 +++++++++++++++++++++++++ .github/workflows/release.yml | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dea1dd50..002838c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,3 +89,31 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 30 + + # Builds a freshly scaffolded consumer project against the local package. + # This exercises the installed-from-npm code path (JSX transform for + # node_modules sources, CJS interop, bundled assets, dependency pins) that + # the workspace build cannot see. + scaffold: + timeout-minutes: 20 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: pnpm/action-setup@v4 + - name: Scaffold a project against the local package + run: | + node packages/starpod/cli/index.mjs new /tmp/scaffold-smoke --yes --name "Scaffold Smoke" --rss "https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml" + node -e "const fs=require('fs');const f='/tmp/scaffold-smoke/package.json';const p=JSON.parse(fs.readFileSync(f));p.dependencies.starpod='file:'+process.env.GITHUB_WORKSPACE+'/packages/starpod';fs.writeFileSync(f,JSON.stringify(p,null,2))" + - name: Build the scaffolded project + working-directory: /tmp/scaffold-smoke + run: | + pnpm install + pnpm exec astro build + - name: Verify agent endpoints in the output + run: | + test -f /tmp/scaffold-smoke/.vercel/output/static/llms.txt + test -f /tmp/scaffold-smoke/.vercel/output/static/openapi.json + node -e "const c=require('/tmp/scaffold-smoke/.vercel/output/config.json');const n=c.routes.filter(r=>JSON.stringify(r).includes('text/markdown')).length;if(!n)throw new Error('no negotiation routes in config.json');console.log('negotiation routes:',n)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..20dc472f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +# Publishes the starpod package to npm. Requires an NPM_TOKEN repository +# secret (an npm automation token with publish rights). +# +# Trigger by pushing a tag like `starpod@0.1.0` (matching the version in +# packages/starpod/package.json), or manually from the Actions tab. +name: Release +on: + workflow_dispatch: + push: + tags: + - 'starpod@*' + +permissions: + contents: read + # For npm provenance attestation. + id-token: write + +jobs: + publish: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + - uses: pnpm/action-setup@v4 + - name: Install dependencies + run: pnpm install + - name: Lint and unit tests + run: | + pnpm lint + pnpm test:unit run + - name: Publish starpod + working-directory: packages/starpod + run: pnpm publish --access public --no-git-checks --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 70c30c792319f3ae77057a18b43ce82127a91a44 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Tue, 25 Aug 2026 22:12:26 -0400 Subject: [PATCH 11/12] Publish via npm OIDC trusted publishing instead of a token secret npm handles the OIDC exchange natively (id-token: write stays; the workflow upgrades npm since Node 22 bundles 10.x and trusted publishing needs 11.5.1+), and provenance comes automatically with trusted-publisher releases. Requires the one-time trusted publisher setup on npmjs.com. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20dc472f..e1cfdfa7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,8 @@ -# Publishes the starpod package to npm. Requires an NPM_TOKEN repository -# secret (an npm automation token with publish rights). +# Publishes the starpod package to npm via OIDC trusted publishing — no +# token secret needed. One-time setup on npmjs.com (package settings → +# Trusted Publisher): GitHub Actions, owner shipshapecode, repository +# starpod, workflow release.yml. Provenance is generated automatically for +# trusted-publisher releases. # # Trigger by pushing a tag like `starpod@0.1.0` (matching the version in # packages/starpod/package.json), or manually from the Actions tab. @@ -12,7 +15,7 @@ on: permissions: contents: read - # For npm provenance attestation. + # Required for the OIDC token exchange with npm. id-token: write jobs: @@ -32,8 +35,9 @@ jobs: run: | pnpm lint pnpm test:unit run + # Node 22 bundles npm 10.x; OIDC trusted publishing needs npm 11.5.1+. + - name: Update npm for trusted publishing + run: npm install -g npm@latest - name: Publish starpod working-directory: packages/starpod - run: pnpm publish --access public --no-git-checks --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public From cf4c93e542f2bc52b8c077850beeda695e8d08a5 Mon Sep 17 00:00:00 2001 From: Robert Wagner Date: Tue, 25 Aug 2026 22:14:34 -0400 Subject: [PATCH 12/12] Set up release-plan for automated releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated by create-release-plan-setup: plan-release.yml opens and updates a Prepare Release PR (changelog + .release-plan.json) from labeled PRs, and publish.yml releases to npm via OIDC trusted publishing with provenance when that PR merges — no token secret. Replaces the hand-rolled tag-based release workflow. RELEASE.md documents the flow; every merged PR needs one of the changelog labels. Co-Authored-By: Claude Fable 5 --- .github/workflows/plan-release.yml | 61 ++ .github/workflows/publish.yml | 40 + .github/workflows/release.yml | 43 - CHANGELOG.md | 1 + RELEASE.md | 27 + package.json | 17 +- pnpm-lock.yaml | 1435 +++++++++++++++++++++++++++- 7 files changed, 1561 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/plan-release.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 RELEASE.md diff --git a/.github/workflows/plan-release.yml b/.github/workflows/plan-release.yml new file mode 100644 index 00000000..b2843ad3 --- /dev/null +++ b/.github/workflows/plan-release.yml @@ -0,0 +1,61 @@ +name: Plan Release +on: + workflow_dispatch: + push: + branches: + - main + - master + pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + types: + - labeled + - unlabeled + +concurrency: + group: plan-release # only the latest one of these should ever be running + cancel-in-progress: true + +jobs: + should-run-release-plan-prepare: + name: Should we run release-plan prepare? + runs-on: ubuntu-latest + outputs: + should-prepare: ${{ steps.should-prepare.outputs.should-prepare }} + steps: + - uses: release-plan/actions/should-prepare-release@v1 + with: + ref: 'main' + id: should-prepare + + create-prepare-release-pr: + name: Create Prepare Release PR + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: should-run-release-plan-prepare + permissions: + contents: write + issues: read + pull-requests: write + if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true' + steps: + - uses: release-plan/actions/prepare@v1 + name: Run release-plan prepare + with: + ref: 'main' + env: + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} + id: explanation + + - uses: peter-evans/create-pull-request@v8 + name: Create Prepare Release PR + with: + commit-message: "Prepare Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'" + labels: "internal" + sign-commits: true + branch: release-preview + title: Prepare Release ${{ steps.explanation.outputs.new-version }} + body: | + This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 + + ----------------------------------------- + + ${{ steps.explanation.outputs.text }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..77725c0b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +# For every push to the primary branch with .release-plan.json modified, +# runs release-plan. + +name: Publish Stable + +on: + workflow_dispatch: + push: + branches: + - main + - master + paths: + - '.release-plan.json' + +concurrency: + group: publish-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + publish: + name: "NPM Publish" + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + attestations: write + + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: 'https://registry.npmjs.org' + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Publish to NPM + run: NPM_CONFIG_PROVENANCE=true pnpm release-plan publish + env: + GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e1cfdfa7..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Publishes the starpod package to npm via OIDC trusted publishing — no -# token secret needed. One-time setup on npmjs.com (package settings → -# Trusted Publisher): GitHub Actions, owner shipshapecode, repository -# starpod, workflow release.yml. Provenance is generated automatically for -# trusted-publisher releases. -# -# Trigger by pushing a tag like `starpod@0.1.0` (matching the version in -# packages/starpod/package.json), or manually from the Actions tab. -name: Release -on: - workflow_dispatch: - push: - tags: - - 'starpod@*' - -permissions: - contents: read - # Required for the OIDC token exchange with npm. - id-token: write - -jobs: - publish: - timeout-minutes: 15 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - registry-url: 'https://registry.npmjs.org' - - uses: pnpm/action-setup@v4 - - name: Install dependencies - run: pnpm install - - name: Lint and unit tests - run: | - pnpm lint - pnpm test:unit run - # Node 22 bundles npm 10.x; OIDC trusted publishing needs npm 11.5.1+. - - name: Update npm for trusted publishing - run: npm install -g npm@latest - - name: Publish starpod - working-directory: packages/starpod - run: npm publish --access public diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..825c32f0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..4b75c8d8 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,27 @@ +# Release Process + +Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged. + +## Preparation + +Since the majority of the actual release process is automated, the remaining tasks before releasing are: + +- correctly labeling **all** pull requests that have been merged since the last release +- updating pull request titles so they make sense to our users + +Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall +guiding principle here is that changelogs are for humans, not machines. + +When reviewing merged PR's the labels to be used are: + +- breaking - Used when the PR is considered a breaking change. +- enhancement - Used when the PR adds a new feature or enhancement. +- bug - Used when the PR fixes a bug included in a previous release. +- documentation - Used when the PR adds or updates documentation. +- internal - Internal changes or things that don't fit in any other category. + +**Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` + +## Release + +Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/shipshapecode/starpod/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR diff --git a/package.json b/package.json index f64d49b2..6258e478 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "starpod-site", - "private": true, - "type": "module", "version": "0.0.1", + "private": true, "repository": { "type": "git", "url": "git+https://github.com/shipshapecode/starpod.git" }, "license": "MIT", + "type": "module", "scripts": { "astro": "astro", "build": "astro check && astro build", @@ -18,9 +18,9 @@ "lint": "eslint . --cache", "lint:fix": "eslint . --fix", "preview": "astro preview", - "start": "astro dev", "publish:atproto": "tsx scripts/publish-atproto-episodes.ts", "publish:atproto:backfill": "tsx scripts/publish-atproto-episodes.ts --backfill", + "start": "astro dev", "test": "concurrently \"pnpm:test:*(!fix)\" --names \"test:\"", "test:e2e": "pnpm exec playwright test", "test:unit": "vitest" @@ -56,6 +56,7 @@ "prettier": "^3.8.3", "prettier-plugin-astro": "^0.14.1", "prettier-plugin-tailwindcss": "^0.7.4", + "release-plan": "^0.18.0", "sharp": "^0.34.5", "tailwindcss": "^4.3.0", "tsx": "^4.22.4", @@ -63,19 +64,19 @@ "vite": "^7.3.3", "vitest": "^4.1.7" }, + "packageManager": "pnpm@10.27.0", "engines": { "node": "^22.0.0" }, - "packageManager": "pnpm@10.27.0", "pnpm": { - "overrides": { - "fast-xml-parser": "4.5.4" - }, "onlyBuiltDependencies": [ "@tailwindcss/oxide", "@vercel/speed-insights", "esbuild", "sharp" - ] + ], + "overrides": { + "fast-xml-parser": "4.5.4" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f670df3..b660fd05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 5.1.4(@babel/core@7.29.7)(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(preact@10.29.2)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) '@astrojs/vercel': specifier: ^10.0.8 - version: 10.0.8(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(react@19.0.0)(rollup@4.60.4) + version: 10.0.8(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(encoding@0.1.13)(react@19.0.0)(rollup@4.60.4) '@bryanguffey/astro-standard-site': specifier: ^1.0.3 version: 1.0.3(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0)) @@ -96,6 +96,9 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.7.4 version: 0.7.4(prettier-plugin-astro@0.14.1)(prettier@3.8.3) + release-plan: + specifier: ^0.18.0 + version: 0.18.0 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -1116,6 +1119,9 @@ packages: '@noble/hashes': optional: true + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1273,6 +1279,10 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -1350,6 +1360,18 @@ packages: cpu: [x64] os: [win32] + '@manypkg/find-root@2.2.3': + resolution: {integrity: sha512-jtEZKczWTueJYHjGpxU3KJQ08Gsrf4r6Q2GjmPp/RGk5leeYAA1eyDADSAF+KVCsQ6EwZd/FMcOFCoMhtqdCtQ==} + engines: {node: '>=14.18.0'} + + '@manypkg/get-packages@2.2.2': + resolution: {integrity: sha512-3+Zd8kLZmsyJFmWTBtY0MAuCErI7yKB2cjMBlujvSVKZ2R/BMXi0kjCXu2dtRlSq/ML86t1FkumT0yreQ3n8OQ==} + engines: {node: '>=14.18.0'} + + '@manypkg/tools@1.1.2': + resolution: {integrity: sha512-3lBouSuF7CqlseLB+FKES0K4FQ02JrbEoRtJhxnsyB1s5v4AP03gsoohN8jp7DcOImhaR9scYdztq3/sLfk/qQ==} + engines: {node: '>=14.18.0'} + '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} @@ -1370,9 +1392,91 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/git@6.0.3': + resolution: {integrity: sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@npmcli/package-json@6.2.0': + resolution: {integrity: sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@npmcli/promise-spawn@8.0.3': + resolution: {integrity: sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.6': + resolution: {integrity: sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.4': + resolution: {integrity: sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.2.2': + resolution: {integrity: sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + + '@octokit/plugin-paginate-rest@11.6.0': + resolution: {integrity: sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.5.0': + resolution: {integrity: sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.8': + resolution: {integrity: sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.2.4': + resolution: {integrity: sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==} + engines: {node: '>= 18'} + + '@octokit/rest@21.1.1': + resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.3.6': resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1382,6 +1486,18 @@ packages: engines: {node: '>=18'} hasBin: true + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@3.0.3': + resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} + engines: {node: '>=12'} + '@preact/preset-vite@2.10.5': resolution: {integrity: sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==} peerDependencies: @@ -1551,6 +1667,9 @@ packages: cpu: [x64] os: [win32] + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -1585,6 +1704,10 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1702,6 +1825,10 @@ packages: peerDependencies: preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0' + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1942,6 +2069,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -1960,6 +2095,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1968,6 +2107,13 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1992,6 +2138,9 @@ packages: array-iterate@2.0.1: resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2061,6 +2210,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -2073,6 +2225,9 @@ packages: brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -2089,6 +2244,10 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2119,6 +2278,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -2136,6 +2299,10 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -2144,6 +2311,18 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2182,6 +2361,9 @@ packages: engines: {node: '>=18'} hasBin: true + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2257,6 +2439,10 @@ packages: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2431,6 +2617,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.364: resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} @@ -2440,6 +2629,12 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + enhanced-resolve@5.22.1: resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} @@ -2460,6 +2655,9 @@ packages: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2582,6 +2780,14 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2589,6 +2795,9 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2630,6 +2839,10 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2676,10 +2889,25 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2712,6 +2940,14 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -2719,6 +2955,11 @@ packages: resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==} engines: {node: '>=20.20.0'} + github-changelog@2.1.4: + resolution: {integrity: sha512-mZQF/YC9OR8XMGpYlLQqG66RiKwlaQZ7rXTZug28oOYkzOXd0acszuvYHVzPlYmns1aDDSwQkbzFxNOYpWhmig==} + engines: {node: 18.* || 20.* || >= 22} + hasBin: true + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -2730,10 +2971,19 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2746,6 +2996,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2809,6 +3062,17 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hosted-git-info@8.1.0: + resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==} + engines: {node: ^18.17.0 || >=20.5.0} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2829,6 +3093,10 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2841,6 +3109,21 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2857,10 +3140,31 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@5.0.0: + resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} + engines: {node: ^18.17.0 || >=20.5.0} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.5.0: + resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} + engines: {node: '>= 12'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -2915,6 +3219,9 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2946,6 +3253,14 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -2954,6 +3269,10 @@ packages: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -2972,13 +3291,23 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + iso-datestring-validator@2.2.2: resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -3006,6 +3335,10 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3026,6 +3359,9 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3036,6 +3372,14 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + ky@1.14.3: + resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} + engines: {node: '>=18'} + + latest-version@9.0.0: + resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} + engines: {node: '>=18'} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -3128,6 +3472,9 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.1: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} @@ -3135,6 +3482,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -3145,6 +3496,10 @@ packages: magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -3197,6 +3552,9 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3297,6 +3655,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -3312,14 +3674,58 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -3333,6 +3739,9 @@ packages: multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3341,6 +3750,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -3383,9 +3796,37 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + npm-install-checks@7.1.2: + resolution: {integrity: sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-package-arg@12.0.2: + resolution: {integrity: sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-pick-manifest@10.0.0: + resolution: {integrity: sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -3411,6 +3852,13 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + oniguruma-parser@0.12.2: resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} @@ -3433,6 +3881,14 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + p-queue@9.3.0: resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==} engines: {node: '>=20'} @@ -3441,6 +3897,13 @@ packages: resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} engines: {node: '>=20'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-json@10.0.1: + resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} + engines: {node: '>=18'} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -3448,9 +3911,25 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-github-repo-url@1.4.1: + resolution: {integrity: sha512-bSWyzBKqcSL4RrncTpGsEKoJ7H8a4L3++ifTAbTFeMHyq2wRV+42DGmQcHIrJIvdcacjIOxEuKH/w4tthF17gg==} + parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -3467,10 +3946,22 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -3607,16 +4098,43 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + promise-limit@2.7.0: resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -3631,6 +4149,10 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -3663,6 +4185,14 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + registry-auth-token@5.1.1: + resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} @@ -3675,6 +4205,10 @@ packages: rehype@13.0.2: resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + release-plan@0.18.0: + resolution: {integrity: sha512-WzP+O+XRF4AqhTDQK84FovY+TxHyC8J7QWoOwxnrvVjyHns417l1FUldboRhBYua5Pej6Yg/2jH1dEH2MRNHeA==} + hasBin: true + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -3728,10 +4262,19 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rollup@4.60.4: resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3753,6 +4296,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} @@ -3826,6 +4372,13 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-code-frame@1.3.0: resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} @@ -3837,10 +4390,22 @@ packages: engines: {node: '>=20.19.5', npm: '>=10.8.2'} hasBin: true + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3859,6 +4424,22 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + stack-trace@1.0.0: resolution: {integrity: sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA==} engines: {node: '>=20.0.0'} @@ -3880,6 +4461,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -3887,11 +4472,27 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} - strip-json-comments@3.1.1: + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -3928,10 +4529,22 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + tar@7.5.15: resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} engines: {node: '>=18'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -4037,12 +4650,22 @@ packages: unicode-segmenter@0.14.5: resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} unifont@0.7.4: resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} @@ -4070,6 +4693,13 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unstorage@1.17.5: resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} peerDependencies: @@ -4152,6 +4782,13 @@ packages: typescript: optional: true + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@6.0.2: + resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} + engines: {node: ^18.17.0 || >=20.5.0} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -4397,6 +5034,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -4410,6 +5052,13 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -4439,6 +5088,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -4457,6 +5109,10 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4465,6 +5121,10 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -4477,6 +5137,10 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -4632,12 +5296,12 @@ snapshots: is-wsl: 3.1.1 which-pm-runs: 1.1.0 - '@astrojs/vercel@10.0.8(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(react@19.0.0)(rollup@4.60.4)': + '@astrojs/vercel@10.0.8(astro@6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0))(encoding@0.1.13)(react@19.0.0)(rollup@4.60.4)': dependencies: '@astrojs/internal-helpers': 0.10.0 '@vercel/analytics': 1.6.1(react@19.0.0) '@vercel/functions': 3.6.1 - '@vercel/nft': 1.10.2(rollup@4.60.4) + '@vercel/nft': 1.10.2(encoding@0.1.13)(rollup@4.60.4) '@vercel/routing-utils': 5.3.3 astro: 6.4.2(@types/node@24.12.4)(@vercel/functions@3.6.1)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(tsx@4.22.4)(yaml@2.9.0) esbuild: 0.27.7 @@ -5283,6 +5947,8 @@ snapshots: '@exodus/bytes@1.15.1': {} + '@gar/promisify@1.1.3': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -5395,6 +6061,15 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 @@ -5476,12 +6151,27 @@ snapshots: '@libsql/win32-x64-msvc@0.5.29': optional: true - '@mapbox/node-pre-gyp@2.0.3': + '@manypkg/find-root@2.2.3': + dependencies: + '@manypkg/tools': 1.1.2 + + '@manypkg/get-packages@2.2.2': + dependencies: + '@manypkg/find-root': 2.2.3 + '@manypkg/tools': 1.1.2 + + '@manypkg/tools@1.1.2': + dependencies: + fast-glob: 3.3.3 + jju: 1.4.0 + js-yaml: 4.1.1 + + '@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)': dependencies: consola: 3.4.2 detect-libc: 2.1.2 https-proxy-agent: 7.0.6 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) nopt: 8.1.0 semver: 7.8.1 tar: 7.5.15 @@ -5503,14 +6193,132 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.8.1 + + '@npmcli/git@6.0.3': + dependencies: + '@npmcli/promise-spawn': 8.0.3 + ini: 5.0.0 + lru-cache: 10.4.3 + npm-pick-manifest: 10.0.0 + proc-log: 5.0.0 + promise-retry: 2.0.1 + semver: 7.8.1 + which: 5.0.0 + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@npmcli/package-json@6.2.0': + dependencies: + '@npmcli/git': 6.0.3 + glob: 10.5.0 + hosted-git-info: 8.1.0 + json-parse-even-better-errors: 4.0.0 + proc-log: 5.0.0 + semver: 7.8.1 + validate-npm-package-license: 3.0.4 + + '@npmcli/promise-spawn@8.0.3': + dependencies: + which: 5.0.0 + + '@octokit/auth-token@5.1.2': {} + + '@octokit/core@6.1.6': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.2 + '@octokit/request': 9.2.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@10.1.4': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@8.2.2': + dependencies: + '@octokit/request': 9.2.4 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@24.2.0': {} + + '@octokit/openapi-types@25.1.0': {} + + '@octokit/plugin-paginate-rest@11.6.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + + '@octokit/plugin-rest-endpoint-methods@13.5.0(@octokit/core@6.1.6)': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/types': 13.10.0 + + '@octokit/request-error@6.1.8': + dependencies: + '@octokit/types': 14.1.0 + + '@octokit/request@9.2.4': + dependencies: + '@octokit/endpoint': 10.1.4 + '@octokit/request-error': 6.1.8 + '@octokit/types': 14.1.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.3 + + '@octokit/rest@21.1.1': + dependencies: + '@octokit/core': 6.1.6 + '@octokit/plugin-paginate-rest': 11.6.0(@octokit/core@6.1.6) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.6) + '@octokit/plugin-rest-endpoint-methods': 13.5.0(@octokit/core@6.1.6) + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + '@oslojs/encoding@1.1.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.3.6': {} '@playwright/test@1.60.0': dependencies: playwright: 1.60.0 + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@3.0.3': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + '@preact/preset-vite@2.10.5(@babel/core@7.29.7)(preact@10.29.2)(rollup@4.60.4)(vite@7.3.3(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 @@ -5645,6 +6453,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true + '@sec-ant/readable-stream@0.4.1': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -5690,6 +6500,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/spec@1.1.0': {} '@tailwindcss/forms@0.5.11(tailwindcss@4.3.0)': @@ -5795,6 +6607,8 @@ snapshots: '@testing-library/dom': 8.20.1 preact: 10.29.2 + '@tootallnate/once@1.1.2': {} + '@types/aria-query@5.0.4': {} '@types/chai@5.2.3': @@ -5906,9 +6720,9 @@ snapshots: dependencies: '@vercel/oidc': 3.5.0 - '@vercel/nft@1.10.2(rollup@4.60.4)': + '@vercel/nft@1.10.2(encoding@0.1.13)(rollup@4.60.4)': dependencies: - '@mapbox/node-pre-gyp': 2.0.3 + '@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13) '@rollup/pluginutils': 5.4.0(rollup@4.60.4) acorn: 8.16.0 acorn-import-attributes: 1.9.5(acorn@8.16.0) @@ -6049,6 +6863,15 @@ snapshots: agent-base@7.1.4: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv-draft-04@1.0.0(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -6069,12 +6892,18 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.3.0: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -6097,6 +6926,8 @@ snapshots: array-iterate@2.0.1: {} + assert-never@1.4.0: {} + assertion-error@2.0.1: {} astro-eslint-parser@1.4.0: @@ -6255,6 +7086,8 @@ snapshots: baseline-browser-mapping@2.10.33: {} + before-after-hook@3.0.2: {} + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -6270,6 +7103,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -6288,6 +7125,29 @@ snapshots: buffer-from@1.1.2: {} + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6318,6 +7178,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -6332,10 +7194,29 @@ snapshots: dependencies: readdirp: 5.0.0 + chownr@2.0.0: {} + chownr@3.0.0: {} ci-info@4.4.0: {} + clean-stack@2.2.0: {} + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.2 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -6371,6 +7252,11 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + consola@3.4.2: {} convert-source-map@2.0.0: {} @@ -6460,6 +7346,8 @@ snapshots: which-collection: 1.0.2 which-typed-array: 1.1.21 + deep-extend@0.6.0: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -6539,6 +7427,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.364: {} emmet@2.4.11: @@ -6548,6 +7438,13 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 @@ -6561,6 +7458,8 @@ snapshots: entities@8.0.0: {} + err-code@2.0.3: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -6805,10 +7704,39 @@ snapshots: eventemitter3@5.0.4: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.2.0 + expect-type@1.3.0: {} extend@3.0.2: {} + fast-content-type-parse@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6847,6 +7775,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -6885,6 +7817,11 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -6893,6 +7830,18 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + fsevents@2.3.2: optional: true @@ -6925,6 +7874,13 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -6933,6 +7889,21 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-changelog@2.1.4: + dependencies: + '@manypkg/get-packages': 2.2.2 + chalk: 4.1.2 + cli-highlight: 2.1.11 + execa: 5.1.1 + hosted-git-info: 4.1.0 + make-fetch-happen: 9.1.0 + p-map: 3.0.0 + progress: 2.0.3 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -6943,18 +7914,38 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@13.0.6: dependencies: minimatch: 10.2.5 minipass: 7.1.3 path-scurry: 2.0.2 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@14.0.0: {} globals@16.5.0: {} gopd@1.2.0: {} + graceful-fs@4.2.10: {} + graceful-fs@4.2.11: {} h3@1.15.11: @@ -7076,6 +8067,16 @@ snapshots: he@1.2.0: {} + highlight.js@10.7.3: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hosted-git-info@8.1.0: + dependencies: + lru-cache: 10.4.3 + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.15.1 @@ -7103,6 +8104,14 @@ snapshots: http-cache-semantics@4.2.0: {} + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -7124,6 +8133,19 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + ignore@5.3.2: {} import-fresh@3.3.1: @@ -7135,12 +8157,27 @@ snapshots: indent-string@4.0.0: {} + infer-owner@1.0.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@5.0.0: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.4 side-channel: 1.1.0 + ip-address@10.5.0: {} + iron-webcrypto@1.2.1: {} is-arguments@1.2.0: @@ -7186,6 +8223,8 @@ snapshots: dependencies: is-docker: 3.0.0 + is-lambda@1.0.1: {} + is-map@2.0.3: {} is-number-object@1.1.1: @@ -7212,6 +8251,10 @@ snapshots: dependencies: call-bound: 1.0.4 + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -7223,6 +8266,8 @@ snapshots: has-symbols: 1.1.0 safe-regex-test: 1.1.0 + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakset@2.0.4: @@ -7238,10 +8283,20 @@ snapshots: isexe@2.0.0: {} + isexe@3.1.5: {} + iso-datestring-validator@2.2.2: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.7.0: {} + jju@1.4.0: {} + js-base64@3.7.8: {} js-tokens@4.0.0: {} @@ -7282,6 +8337,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@4.0.0: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -7294,6 +8351,12 @@ snapshots: jsonc-parser@3.3.1: {} + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -7302,6 +8365,12 @@ snapshots: kolorist@1.8.0: {} + ky@1.14.3: {} + + latest-version@9.0.0: + dependencies: + package-json: 10.0.1 + leac@0.6.0: {} levn@0.4.1: @@ -7381,12 +8450,18 @@ snapshots: longest-streak@3.1.0: {} + lru-cache@10.4.3: {} + lru-cache@11.5.1: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lz-string@1.5.0: {} magic-string@0.30.21: @@ -7399,6 +8474,28 @@ snapshots: '@babel/types': 7.29.7 source-map-js: 1.2.1 + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.2.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + markdown-table@3.0.4: {} math-intrinsics@1.1.0: {} @@ -7527,6 +8624,8 @@ snapshots: mdn-data@2.27.1: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -7731,6 +8830,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mimic-fn@2.1.0: {} + min-indent@1.0.1: {} mini-svg-data-uri@1.4.4: {} @@ -7743,12 +8844,55 @@ snapshots: dependencies: brace-expansion: 1.1.15 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + minizlib@3.1.0: dependencies: minipass: 7.1.3 + mkdirp@1.0.4: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -7757,10 +8901,18 @@ snapshots: multiformats@9.9.0: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.12: {} natural-compare@1.4.0: {} + negotiator@0.6.4: {} + neotraverse@0.6.18: {} nlcst-to-string@4.0.0: @@ -7769,9 +8921,11 @@ snapshots: node-fetch-native@1.6.7: {} - node-fetch@2.7.0: + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 node-gyp-build@4.8.4: {} @@ -7790,10 +8944,41 @@ snapshots: normalize-path@3.0.0: {} + npm-install-checks@7.1.2: + dependencies: + semver: 7.8.1 + + npm-normalize-package-bin@4.0.0: {} + + npm-package-arg@12.0.2: + dependencies: + hosted-git-info: 8.1.0 + proc-log: 5.0.0 + semver: 7.8.1 + validate-npm-package-name: 6.0.2 + + npm-pick-manifest@10.0.0: + dependencies: + npm-install-checks: 7.1.2 + npm-normalize-package-bin: 4.0.0 + npm-package-arg: 12.0.2 + semver: 7.8.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 + object-assign@4.1.1: {} + object-inspect@1.13.4: {} object-is@1.1.6: @@ -7822,6 +9007,14 @@ snapshots: ohash@2.0.11: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + oniguruma-parser@0.12.2: {} oniguruma-to-es@4.3.6: @@ -7851,6 +9044,14 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@3.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + p-queue@9.3.0: dependencies: eventemitter3: 5.0.4 @@ -7858,12 +9059,23 @@ snapshots: p-timeout@7.0.1: {} + package-json-from-dist@1.0.1: {} + + package-json@10.0.1: + dependencies: + ky: 1.14.3 + registry-auth-token: 5.1.1 + registry-url: 6.0.1 + semver: 7.8.1 + package-manager-detector@1.6.0: {} parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-github-repo-url@1.4.1: {} + parse-latin@7.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -7873,6 +9085,16 @@ snapshots: unist-util-visit-children: 3.0.0 vfile: 6.0.3 + parse-ms@4.0.0: {} + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -7890,8 +9112,17 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-scurry@2.0.2: dependencies: lru-cache: 11.5.1 @@ -7967,12 +9198,29 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + prismjs@1.30.0: {} + proc-log@5.0.0: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1: {} + promise-limit@2.7.0: {} + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + property-information@7.1.0: {} + proto-list@1.2.4: {} + proxy-from-env@2.1.0: {} punycode@2.3.1: {} @@ -7981,6 +9229,13 @@ snapshots: radix3@1.1.2: {} + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-is@17.0.2: {} react@19.0.0: @@ -8014,6 +9269,14 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + registry-auth-token@5.1.1: + dependencies: + '@pnpm/npm-conf': 3.0.3 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + rehype-parse@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -8039,6 +9302,26 @@ snapshots: rehype-stringify: 10.0.1 unified: 11.0.5 + release-plan@0.18.0: + dependencies: + '@manypkg/get-packages': 2.2.2 + '@npmcli/package-json': 6.2.0 + '@octokit/rest': 21.1.1 + assert-never: 1.4.0 + chalk: 5.6.2 + cli-highlight: 2.1.11 + execa: 9.6.1 + fs-extra: 11.4.0 + github-changelog: 2.1.4 + js-yaml: 4.1.1 + latest-version: 9.0.0 + parse-github-repo-url: 1.4.1 + semver: 7.8.1 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - supports-color + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -8119,8 +9402,14 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 + retry@0.12.0: {} + reusify@1.1.0: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 @@ -8176,6 +9465,9 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: + optional: true + sass-formatter@0.7.9: dependencies: suf-log: 2.5.3 @@ -8292,6 +9584,10 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + simple-code-frame@1.3.0: dependencies: kolorist: 1.8.0 @@ -8305,8 +9601,23 @@ snapshots: arg: 5.0.2 sax: 1.6.0 + smart-buffer@4.2.0: {} + smol-toml@1.6.1: {} + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks@2.8.9: + dependencies: + ip-address: 10.5.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -8320,6 +9631,24 @@ snapshots: space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + stack-trace@1.0.0: {} stackback@0.0.2: {} @@ -8339,6 +9668,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -8348,10 +9683,20 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} strnum@1.1.2: {} @@ -8388,6 +9733,15 @@ snapshots: tapable@2.3.3: {} + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + tar@7.5.15: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -8396,6 +9750,14 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + tiny-inflate@1.0.3: {} tinybench@2.9.0: {} @@ -8477,6 +9839,8 @@ snapshots: unicode-segmenter@0.14.5: {} + unicorn-magic@0.3.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -8493,6 +9857,14 @@ snapshots: ofetch: 1.5.1 ohash: 2.0.11 + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + unist-util-find-after@5.0.0: dependencies: '@types/unist': 3.0.3 @@ -8535,6 +9907,10 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + universal-user-agent@7.0.3: {} + + universalify@2.0.1: {} + unstorage@1.17.5(@vercel/functions@3.6.1): dependencies: anymatch: 3.1.3 @@ -8564,6 +9940,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@6.0.2: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -8789,6 +10172,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@5.0.0: + dependencies: + isexe: 3.1.5 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -8802,6 +10189,14 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + ws@8.21.0: {} xml-name-validator@5.0.0: {} @@ -8814,6 +10209,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yallist@5.0.0: {} yaml-language-server@1.20.0: @@ -8834,10 +10231,22 @@ snapshots: yaml@2.9.0: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} yargs-parser@22.0.0: {} + yargs@16.2.2: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -8852,6 +10261,8 @@ snapshots: yocto-queue@1.2.2: {} + yoctocolors@2.2.0: {} + zimmerframe@1.1.4: {} zod@3.25.76: {}