diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6a398ea --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +# Build output and tool caches +dist +.netlify +.tanstack +.output +src/routeTree.gen.ts + +# Golden files: tests compare these byte for byte +tests/fixtures + +# Vendored font binaries and their licence +assets/fonts + +package-lock.json diff --git a/assets/fonts/Inter-Bold.woff b/assets/fonts/Inter-Bold.woff new file mode 100644 index 0000000..23882e4 Binary files /dev/null and b/assets/fonts/Inter-Bold.woff differ diff --git a/assets/fonts/Inter-Regular.woff b/assets/fonts/Inter-Regular.woff new file mode 100644 index 0000000..df5bd3b Binary files /dev/null and b/assets/fonts/Inter-Regular.woff differ diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..21f6aff --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/content/episodes/2024-01.md b/content/episodes/2024-01.md index fd58589..9928ad2 100644 --- a/content/episodes/2024-01.md +++ b/content/episodes/2024-01.md @@ -161,7 +161,7 @@ And that combined with TypeScript really taking over as the definitive way to wr And then maybe some of the learnings from running a language like that will be brought back into JavaScript, the ECMAScript process, or into TypeScript itself. Love to see it. [00:07:56] - [00:08:07] +[00:08:07] ## [React Libraries for 2024 by Robin Wieruch](https://www.robinwieruch.de/react-libraries/) @@ -288,7 +288,7 @@ And very cool to have your source maps PR ship out. Finally! Ha ha ha. Looking f **Carl Vitullo:** Well, hopefully it'll make the stack traces easier to read anyway. [00:22:30] - [00:22:33] +[00:22:33] ## [Remix SPA mode](https://github.com/remix-run/remix/pull/8338) diff --git a/content/episodes/2024-06.md b/content/episodes/2024-06.md index b5fc7a4..534ced5 100644 --- a/content/episodes/2024-06.md +++ b/content/episodes/2024-06.md @@ -629,7 +629,7 @@ Lock will let you pin your dependencies, left the, you know, the gap of what abo So being able to pin a version, super great. Yeah. As well as like having a env loader, so we don't need to use env anymore. But yeah, so I appreciated this as a quick summary of a number of new built ins for Node. [00:57:06] - [00:57:18] +[00:57:18] ## [Migrating to the Next App Router With Zero Downtime](https://workos.com/blog/migrating-to-next-js-app-router-with-zero-downtime) diff --git a/content/episodes/2024-07.md b/content/episodes/2024-07.md index e983113..7aab023 100644 --- a/content/episodes/2024-07.md +++ b/content/episodes/2024-07.md @@ -137,7 +137,7 @@ Yeah, speaking of layoffs, layoffs. fyi. This is the first time in, like, the A **Carl Vitullo:** I've never quite understood the niche, the purpose that Storybook really serves. But, you know, hey, minor release, so it seemed worth calling out. I know it's pretty widely used. [00:04:42] - [00:04:52] +[00:04:52] ## [React Native Reanimated 3.14](https://github.com/software-mansion/) @@ -163,7 +163,7 @@ So this is a Rust based developer being built by the team that put together one **Mark Erikson:** React Rally is wonderful. They've done a great job of really having a good community vibe over the years. I should be there. So folks want to come by and say hi, please do. [00:06:16] - [00:06:24] +[00:06:24] ## [React Universe](https://www.reactuniverseconf.com/) diff --git a/content/episodes/2025-02.md b/content/episodes/2025-02.md index 352ba57..d956b41 100644 --- a/content/episodes/2025-02.md +++ b/content/episodes/2025-02.md @@ -167,7 +167,7 @@ I've done a lot of maintenance of legacy apps and weird edge cases like that, li ## [Prettier 3.5](https://prettier.io/blog/2025/02/09/3.5.0) - [00:09:01] +[00:09:01] **Carl Vitullo:** Cool, new version of Prettier. I saw they're adding an option, which they do not do very frequently, so that's cool. [00:09:01] @@ -455,7 +455,7 @@ And so Ben's pretty excited about that. Also on, also on the note of just, you k ## [JavaScript Temporal is coming](https://developer.mozilla.org/en-US/blog/javascript-temporal-is-coming/) - [00:49:52] +[00:49:52] **Carl Vitullo:** Mozilla had a post at the end of January, about Temporal. JavaScript Temporal is coming. Dates in JavaScript are garbage. They are awful. My favorite example of a specific way they're awful is that they have no concept of time zone. And so every date uses your local system time zone, Which is absolutely awful. [00:49:53] diff --git a/docs/superpowers/plans/2026-09-08-content-pipeline.md b/docs/superpowers/plans/2026-09-08-content-pipeline.md index 92d8183..d7c4884 100644 --- a/docs/superpowers/plans/2026-09-08-content-pipeline.md +++ b/docs/superpowers/plans/2026-09-08-content-pipeline.md @@ -27,22 +27,22 @@ ## File Structure -| Path | Responsibility | -| --- | --- | -| `package.json` | `type: module`, `private`, `test` script, deps | -| `tsconfig.json` | Editor config only (`noEmit`, `allowImportingTsExtensions`, `erasableSyntaxOnly`) | -| `.gitignore` | `node_modules`, `.DS_Store`, `*.m4a` | -| `src/content/slug.ts` | `slug()`, `normalizeTime()`, `flattenLinks()` — the shared text rules | -| `src/content/parse.ts` | Types (`Episode`, `OutlineItem`, `Section`, `Segment`), `splitFile()`, `parseEpisode()` | -| `src/content/serialize.ts` | `serializeEpisodeFile()` — front matter + untouched body back to a string | -| `src/content/srt.ts` | `toSrt(episode)` | -| `scripts/migrate.ts` | One-time conversion of `reactiflux.com/src/transcripts/tmir-*.md` | -| `scripts/ingest.ts` | Transistor feed → ingest-owned front matter | -| `scripts/publish-transcript.ts` | Descript export → file body; SRT → Transistor | -| `scripts/publish-atproto.ts` | `site.standard.publication` + `site.standard.document` records → the show's PDS; `atUri` → front matter | -| `.env` (gitignored), `.env.example` | every credential and build variable, loaded by `node --env-file-if-exists=.env` | -| `tests/*.test.ts` | One test file per module/script | -| `tests/fixtures/*.md`, `tests/fixtures/feed.xml` | Short real excerpts of each historical format | +| Path | Responsibility | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | +| `package.json` | `type: module`, `private`, `test` script, deps | +| `tsconfig.json` | Editor config only (`noEmit`, `allowImportingTsExtensions`, `erasableSyntaxOnly`) | +| `.gitignore` | `node_modules`, `.DS_Store`, `*.m4a` | +| `src/content/slug.ts` | `slug()`, `normalizeTime()`, `flattenLinks()` — the shared text rules | +| `src/content/parse.ts` | Types (`Episode`, `OutlineItem`, `Section`, `Segment`), `splitFile()`, `parseEpisode()` | +| `src/content/serialize.ts` | `serializeEpisodeFile()` — front matter + untouched body back to a string | +| `src/content/srt.ts` | `toSrt(episode)` | +| `scripts/migrate.ts` | One-time conversion of `reactiflux.com/src/transcripts/tmir-*.md` | +| `scripts/ingest.ts` | Transistor feed → ingest-owned front matter | +| `scripts/publish-transcript.ts` | Descript export → file body; SRT → Transistor | +| `scripts/publish-atproto.ts` | `site.standard.publication` + `site.standard.document` records → the show's PDS; `atUri` → front matter | +| `.env` (gitignored), `.env.example` | every credential and build variable, loaded by `node --env-file-if-exists=.env` | +| `tests/*.test.ts` | One test file per module/script | +| `tests/fixtures/*.md`, `tests/fixtures/feed.xml` | Short real excerpts of each historical format | ## Canonical Episode File Format @@ -99,6 +99,7 @@ Rules: ### Task 1: Repo bootstrap and the shared text rules **Files:** + - Create: `package.json` - Create: `tsconfig.json` - Create: `.gitignore` @@ -106,6 +107,7 @@ Rules: - Test: `tests/slug.test.ts` **Interfaces:** + - Consumes: nothing. - Produces: - `slug(title: string): string` @@ -205,7 +207,10 @@ test("slug matches anchors published in existing transcripts", () => { }); test("slug keeps underscores and digits, drops everything else non-word", () => { - assert.equal(slug("React Strict DOM, Why is it so Great?"), "react-strict-dom-why-is-it-so-great"); + assert.equal( + slug("React Strict DOM, Why is it so Great?"), + "react-strict-dom-why-is-it-so-great", + ); assert.equal(slug("Node.js v22"), "nodejs-v22"); }); @@ -292,32 +297,70 @@ Co-Authored-By: Claude Fable 5.1 " ### Task 2: Episode parser and serializer **Files:** + - Create: `src/content/parse.ts` - Create: `src/content/serialize.ts` - Create: `tests/fixtures/canonical-2026-05.md` - Test: `tests/parse.test.ts` **Interfaces:** + - Consumes: `slug`, `flattenLinks`, `normalizeTime` from `src/content/slug.ts`. - Produces: + ```ts - export interface Person { name: string; role?: string; href?: string; img?: string } - export interface OutlineItem { title: string; url?: string; time?: string; anchor: string; children: OutlineItem[] } - export interface Segment { speaker: string; time?: string; text: string } - export interface Section { title: string; anchor: string; time?: string; segments: Segment[] } + export interface Person { + name: string; + role?: string; + href?: string; + img?: string; + } + export interface OutlineItem { + title: string; + url?: string; + time?: string; + anchor: string; + children: OutlineItem[]; + } + export interface Segment { + speaker: string; + time?: string; + text: string; + } + export interface Section { + title: string; + anchor: string; + time?: string; + segments: Segment[]; + } export interface Episode { - slug: string; title: string; date: string; description: string; - time?: string; location?: string; - transistorId?: string; audioUrl?: string; duration?: number; - season?: number; episode?: number; people: Person[]; - bskyPostUrl?: string; // written by ingest (Task 4) - atUri?: string; // written by publish-atproto (Task 7), never by ingest - outline: OutlineItem[]; sections: Section[]; + slug: string; + title: string; + date: string; + description: string; + time?: string; + location?: string; + transistorId?: string; + audioUrl?: string; + duration?: number; + season?: number; + episode?: number; + people: Person[]; + bskyPostUrl?: string; // written by ingest (Task 4) + atUri?: string; // written by publish-atproto (Task 7), never by ingest + outline: OutlineItem[]; + sections: Section[]; } - export function splitFile(text: string): { frontMatter: Record; body: string } - export function parseOutline(region: string): OutlineItem[] - export function parseEpisode(text: string, epSlug: string): Episode - export function serializeEpisodeFile(frontMatter: Record, body: string): string // from serialize.ts + export function splitFile(text: string): { + frontMatter: Record; + body: string; + }; + export function parseOutline(region: string): OutlineItem[]; + export function parseEpisode(text: string, epSlug: string): Episode; + export function serializeEpisodeFile( + frontMatter: Record, + body: string, + ): string; // from serialize.ts ``` - [ ] **Step 1: Create the canonical fixture** @@ -451,7 +494,10 @@ test("speaker carries forward across consecutive paragraphs", () => { assert.equal(segs[0].speaker, "Carl Vitullo"); assert.equal(segs[0].time, "00:00:55"); assert.equal(segs[1].speaker, "Carl Vitullo"); - assert.equal(segs[1].text, "Stuff has changed. Stuff will continue to change. Just like life."); + assert.equal( + segs[1].text, + "Stuff has changed. Stuff will continue to change. Just like life.", + ); assert.equal(segs[2].speaker, "Mark Erikson"); }); @@ -465,7 +511,11 @@ test("serialize writes a changed field without disturbing the body", () => { frontMatter.duration = 5000; const out = serializeEpisodeFile(frontMatter, body); assert.match(out, /^duration: 5000$/m); - assert.ok(out.includes("**Mark Erikson:** Yeah, that sounds all too real. [00:01:44]")); + assert.ok( + out.includes( + "**Mark Erikson:** Yeah, that sounds all too real. [00:01:44]", + ), + ); }); ``` @@ -715,6 +765,7 @@ Co-Authored-By: Claude Fable 5.1 " ### Task 3: Migrate historical transcripts **Files:** + - Create: `scripts/migrate.ts` - Create: `tests/fixtures/legacy-2023-march.md` - Create: `tests/fixtures/legacy-2024-03.md` @@ -723,9 +774,10 @@ Co-Authored-By: Claude Fable 5.1 " - Create: `tests/fixtures/legacy-2026-05.md` - Test: `tests/migrate.test.ts` -**Where the transcript starts.** The boundary is the first *speaker paragraph* — a block matching `^(\[\d…\]\s*)?\*\*[^*]+:\*\*`. But a `##` section heading can sit directly above that paragraph (as in `tmir-2024-03.md`, whose transcript opens with `## Layoffs (seems better!)` before the first `**Carl Vitullo:**` block in some months), so after finding the first speaker paragraph, walk *backwards* over any contiguous `##` heading blocks and include them in the transcript. Walk back over `##` only, never `#`: a lone `#` heading immediately above the transcript is the old transcript-start marker (`# Interview`, `# This Month in React: June, 2025`, `# This Month in React 2023 April`) and is dropped silently, while an `#` heading anywhere else in the outline region (`# Main Content` in `tmir-2024-04.md`) is dropped with a warning. +**Where the transcript starts.** The boundary is the first _speaker paragraph_ — a block matching `^(\[\d…\]\s*)?\*\*[^*]+:\*\*`. But a `##` section heading can sit directly above that paragraph (as in `tmir-2024-03.md`, whose transcript opens with `## Layoffs (seems better!)` before the first `**Carl Vitullo:**` block in some months), so after finding the first speaker paragraph, walk _backwards_ over any contiguous `##` heading blocks and include them in the transcript. Walk back over `##` only, never `#`: a lone `#` heading immediately above the transcript is the old transcript-start marker (`# Interview`, `# This Month in React: June, 2025`, `# This Month in React 2023 April`) and is dropped silently, while an `#` heading anywhere else in the outline region (`# Main Content` in `tmir-2024-04.md`) is dropped with a warning. **Interfaces:** + - Consumes: `normalizeTime`, `slug`, `flattenLinks` from `src/content/slug.ts`; `parseEpisode` from `src/content/parse.ts`; `serializeEpisodeFile` from `src/content/serialize.ts`. - Produces: - `slugFromFilename(name: string): string | null` — `"tmir-march-2023.md"` → `"2023-03"`, `"tmir-2024-03.md"` → `"2024-03"` @@ -735,15 +787,15 @@ Co-Authored-By: Claude Fable 5.1 " There is no clean three-era split — the markers vary per file. Detect each feature independently rather than branching on an era: -| Feature | Variants observed | -| --- | --- | -| Header block | ` blocks. */ -function stripEmbeds(body: string): string { - return body - .replace(/ - - - -- [[01:29](#job-market-fred-data-layoffsfyi)] Job market: [FRED data](https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE), [Layoffs.fyi](https://layoffs.fyi/) -- [[04:17](#new-releases)] New releases - - [[04:26](#react-query-563)] [React query 5.63](https://bsky.app/profile/tkdodo.eu/post/3lfaeteulds2i) - -**Mark:** am making the news. [01:26] - -**Carl:** making the news and then reporting on it. Cool. - -## Job market: [FRED data](https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE), [Layoffs.fyi](https://layoffs.fyi/) - -**Carl:** Yeah, let's start off briefly, overview of the job market. [01:50] - -But conferences still don't have a ton of queued up ones. diff --git a/tests/fixtures/legacy-2025-06.md b/tests/fixtures/legacy-2025-06.md deleted file mode 100644 index 9ed488f..0000000 --- a/tests/fixtures/legacy-2025-06.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "TMiR 2025-06: kinda quiet tbh. ES2025 finalized, new Safari" -date: 2025-06-30 -time: 10am PT / 5PM GMT -location: Main Stage on Reactiflux -description: "Join Carl, Mark, and Mo as we break down This Month in React." -people: "[Carl](https://twitter.com/vcarl_), [Mark](https://twitter.com/acemarke), and [Mo](https://twitter.com/mo__javad)" ---- - - - - - -We had some problems with transcript generation this month, downstream of the audio recording issues. Sorry! Fixed for July. - -- Job market [FRED data](https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE), [Layoffs.fyi](https://layoffs.fyi/) -- New releases - - [Recharts v3.0](https://github.com/recharts/recharts/releases/tag/v3.0.0) - -# This Month in React: June, 2025 - -[00:00] **1-vcarl:** Hi, Carl here. Just a quick note before we get into this episode. - -[00:51] **1-vcarl:** Endlessly! - -## [00:52] job market - -[00:52] **1-vcarl:** Start off with a little bit of job market news. - -## [02:19] New Releases - -[02:19] **1-vcarl:** Yeah. New releases. diff --git a/tests/fixtures/legacy-2025-09.md b/tests/fixtures/legacy-2025-09.md deleted file mode 100644 index f42095a..0000000 --- a/tests/fixtures/legacy-2025-09.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: "TMiR 2025-09: React 19.2 on the horizon; npm is still getting compromised" -date: 2025-09-27 -time: 10am PT / 5PM GMT -location: Main Stage on Reactiflux -description: "Join Carl, Mark, and Mo as we break down This Month in React." -people: "[Carl](https://twitter.com/vcarl_), [Mark](https://twitter.com/acemarke), and [Mo](https://twitter.com/mo__javad)" ---- - - - -- [[00:52](#new-releases)] New releases - - [[00:54](#tanstack-start-10-rc)] [TanStack Start 1.0 RC!](https://tanstack.com/blog/announcing-tanstack-start-v1) - -Carl: Alright. Hello everyone. Thank you for joining us for the September edition of this Month In React. [00:00] - -Mark: Hi, I'm Mark. My day job is working at Replay.io. [00:23] - -Mo: And my name is Mo. [00:37] - -Carl: Yeah, more on that later. [00:51] - -## New releases - -Carl: Let's jump into some new releases. [00:52] diff --git a/tests/fixtures/legacy-2026-05.md b/tests/fixtures/legacy-2026-05.md deleted file mode 100644 index 96d66d9..0000000 --- a/tests/fixtures/legacy-2026-05.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "TMiR 2026-05: Who even is on the Core team anymore, TanStack got pwn'd bad" -date: 2026-05-28 -time: 2pm PT / 9pm GMT -location: Main Stage on Reactiflux -description: "Join Carl Vitullo and Mark Erikson as we discuss what the heck is going on at the React Core team." -people: "[Carl](https://bsky.app/profile/vcarl.com), [Mark](https://bsky.app/profile/acemarke.dev)" ---- - - - - - -- [[00:00:55](#some-podcast-meta)] Some podcast meta -- [[00:01:49](#new-releases)] New Releases - - [[00:01:51](#ts-v7-beta)] [TS v7 beta](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/) -- [[01:08:39](#outro)] Outro - -# Interview - -**Carl Vitullo:** Thank you for joining us. We're coming to you live from Reactiflux. [00:00:00] - -## Some podcast meta - -**Carl Vitullo:** Okay, let's get into it. [00:00:55] - -Stuff has changed. Stuff will continue to change. Just like life. [00:01:40] - -## New Releases - -**Carl Vitullo:** But yeah, okay, into some new releases. [00:01:49] diff --git a/tests/headings.test.ts b/tests/headings.test.ts index adc4a82..a4cb258 100644 --- a/tests/headings.test.ts +++ b/tests/headings.test.ts @@ -45,9 +45,24 @@ test("items with no later paragraph, or sharing a target, are skipped", () => { test("paragraphs without a timestamp are never a target", () => { assert.deepEqual( - insertHeadings(["no time here", "later [00:02:00]"], [ - { time: "00:00:00", title: "Intro" }, - ]), + insertHeadings( + ["no time here", "later [00:02:00]"], + [{ time: "00:00:00", title: "Intro" }], + ), ["no time here", "## Intro", "later [00:02:00]"], ); }); + +test("an out-of-order outline item doesn't overwrite a heading already placed", () => { + assert.deepEqual( + insertHeadings( + ["p0 [00:00:10]", "p1 [00:01:00]", "p2 [00:05:00]"], + [ + { time: "00:04:00", title: "Late" }, + { time: "00:00:05", title: "Early" }, + { time: "00:04:30", title: "Late2" }, + ], + ), + ["## Early", "p0 [00:00:10]", "p1 [00:01:00]", "## Late", "p2 [00:05:00]"], + ); +}); diff --git a/tests/ingest.test.ts b/tests/ingest.test.ts index 46a6b5c..2f74db4 100644 --- a/tests/ingest.test.ts +++ b/tests/ingest.test.ts @@ -4,23 +4,40 @@ import { readFileSync } from "node:fs"; import { parseFeed, slugFromTitle, applyFeedItem } from "../scripts/ingest.ts"; import { parseEpisode } from "../src/content/parse.ts"; -const xml = readFileSync(new URL("./fixtures/feed.xml", import.meta.url), "utf8"); +const xml = readFileSync( + new URL("./fixtures/feed.xml", import.meta.url), + "utf8", +); test("slugFromTitle handles every TMiR title style and rejects the rest", () => { - assert.equal(slugFromTitle("TMiR 2026-05: Who even is on the Core team anymore"), "2026-05"); - assert.equal(slugFromTitle("TMiR 2023-10: React Forget, Canary Releases"), "2023-10"); - assert.equal(slugFromTitle("This Month in React – September 2023"), "2023-09"); + assert.equal( + slugFromTitle("TMiR 2026-05: Who even is on the Core team anymore"), + "2026-05", + ); + assert.equal( + slugFromTitle("TMiR 2023-10: React Forget, Canary Releases"), + "2023-10", + ); + assert.equal( + slugFromTitle("This Month in React – September 2023"), + "2023-09", + ); assert.equal(slugFromTitle("This Month In React – March 2023"), "2023-03"); assert.equal(slugFromTitle("This Month in React (April 2023)"), "2023-04"); assert.equal( - slugFromTitle("TMiR 2024-09 – Async Components??, a React 19 cheatsheet, static Hermes, and trademarks drama"), + slugFromTitle( + "TMiR 2024-09 – Async Components??, a React 19 cheatsheet, static Hermes, and trademarks drama", + ), "2024-09", ); assert.equal( slugFromTitle("Mark & Carl talk with Swizec Teller about using AI at work"), "2026-04", ); - assert.equal(slugFromTitle("Office Hours – States of Burnout with Jenny Truong"), null); + assert.equal( + slugFromTitle("Office Hours – States of Burnout with Jenny Truong"), + null, + ); assert.equal(slugFromTitle("Behind the React Documentary"), null); }); @@ -48,7 +65,8 @@ test("parseFeed extracts TMiR items only", () => { img: "https://img.transistorcdn.com/carl.jpg", }, ], - bskyPostUrl: "https://bsky.app/profile/thismonthinreact.com/post/3lqz7abcd2k2x", + bskyPostUrl: + "https://bsky.app/profile/thismonthinreact.com/post/3lqz7abcd2k2x", }); assert.equal(items[1].slug, "2023-09"); assert.equal(items[1].people.length, 1); @@ -95,3 +113,41 @@ test("applyFeedItem writes only ingest-owned fields and is idempotent", () => { // atUri belongs to publish-atproto; ingest must leave it exactly as it found it assert.equal(ep.atUri, "at://did:plc:example/site.standard.document/2026-05"); }); + +test("parseFeed accepts HH:MM:SS and MM:SS itunes:duration", () => { + const feed = (duration: string) => + [ + "", + "TMiR 2026-06: Colon durations", + '', + `${duration}`, + "", + ].join("\n"); + + assert.equal(parseFeed(feed("1:09:58"))[0]?.duration, 4198); + assert.equal(parseFeed(feed("09:58"))[0]?.duration, 598); + assert.equal(parseFeed(feed("4198"))[0]?.duration, 4198); +}); + +test("applyFeedItem does not clobber curated people with an empty feed list", () => { + const file = [ + "---", + 'title: "TMiR 2026-05: test"', + "date: 2026-05-28", + 'description: "d"', + "people:", + " - name: Carl Vitullo", + " role: Producer", + "---", + "", + "# Transcript", + "", + "**Carl Vitullo:** Hello. [00:00:55]", + "", + ].join("\n"); + + const item = { ...parseFeed(xml)[0], people: [] }; + const ep = parseEpisode(applyFeedItem(file, item), "2026-05"); + assert.equal(ep.people.length, 1); + assert.equal(ep.people[0].name, "Carl Vitullo"); +}); diff --git a/tests/jsonld.test.ts b/tests/jsonld.test.ts new file mode 100644 index 0000000..ac3ac66 --- /dev/null +++ b/tests/jsonld.test.ts @@ -0,0 +1,14 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { jsonLd } from "../src/content/jsonld.ts"; + +test("a in the data cannot close the block", () => { + const out = jsonLd({ + description: "beware ", + }); + assert.ok(!out.includes("<")); + assert.equal( + JSON.parse(out).description, + "beware ", + ); +}); diff --git a/tests/load.test.ts b/tests/load.test.ts index 3a7657f..91aeb1b 100644 --- a/tests/load.test.ts +++ b/tests/load.test.ts @@ -13,18 +13,12 @@ test("loadEpisodes reads and parses the content directory", async () => { assert.equal(episodes[0].sections.length, 2); }); -test("loadEpisodes sorts by date descending", async () => { - const dates = (await loadEpisodes()).map((e) => e.date); - assert.deepEqual(dates, [...dates].sort().reverse()); -}); - test("getEpisode finds by slug and misses cleanly", async () => { assert.equal((await getEpisode("2026-05"))?.slug, "2026-05"); assert.equal(await getEpisode("1999-01"), undefined); }); test("loadEpisodes breaks a date tie by slug descending", async () => { - const { loadEpisodes: load } = await import("../src/content/load.ts"); const tied = [ { slug: "2026-06", date: "2026-06-24" }, { slug: "2026-07", date: "2026-06-24" }, @@ -34,5 +28,4 @@ test("loadEpisodes breaks a date tie by slug descending", async () => { tied.sort(episodeOrder).map((e) => e.slug), ["2026-07", "2026-06", "2026-05"], ); - assert.ok(await load()); }); diff --git a/tests/migrate.test.ts b/tests/migrate.test.ts deleted file mode 100644 index 62fff8e..0000000 --- a/tests/migrate.test.ts +++ /dev/null @@ -1,307 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { - convert, - mergeExistingFrontMatter, - normalizeSpeakerName, - slugFromFilename, -} from "../scripts/migrate.ts"; -import { parseEpisode, splitFile } from "../src/content/parse.ts"; - -const fixture = (name: string) => - readFileSync(new URL(`./fixtures/${name}`, import.meta.url), "utf8"); - -test("slugFromFilename maps both filename styles", () => { - assert.equal(slugFromFilename("tmir-2024-03.md"), "2024-03"); - assert.equal(slugFromFilename("tmir-march-2023.md"), "2023-03"); - assert.equal(slugFromFilename("tmir-april-2023.md"), "2023-04"); - assert.equal(slugFromFilename("tmir-dec-2023.md"), "2023-12"); - assert.equal(slugFromFilename("tmir-nov-2023.md"), "2023-11"); - assert.equal(slugFromFilename("tmir-october-2023.md"), "2023-10"); - assert.equal(slugFromFilename("ali-spittel.md"), null); -}); - -test("2023 format: iframe stripped, outline synthesized from headings", () => { - const { text, warnings } = convert(fixture("legacy-2023-march.md")); - assert.ok(!text.includes(" { - const { text } = convert(fixture("legacy-2024-03.md")); - assert.ok(!text.includes("[Spotify](")); - assert.ok(!text.includes(" { - const { text } = convert(fixture("legacy-2025-01.md")); - assert.ok(!text.includes("