Refactor site generation into a modular content pipeline
Deploy website / deploy (push) Successful in 24s
Deploy website / deploy (push) Successful in 24s
This commit is contained in:
1 parent
0f69028d4a
commit
146d516f24
20 files changed
+826
-427
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { createRegenerationQueue } from '../dev-reload.js'
|
||||
|
||||
const waitFor = async (predicate) => {
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
if (predicate()) return
|
||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
||||
}
|
||||
assert.fail('timed out waiting for generation')
|
||||
}
|
||||
|
||||
test('rapid edits coalesce and only the latest generation reloads', async () => {
|
||||
const pending = []
|
||||
const events = []
|
||||
const queue = createRegenerationQueue({
|
||||
generate: () => new Promise((resolve) => pending.push(resolve)),
|
||||
onSuccess: () => events.push('reload'),
|
||||
onError: () => events.push('error'),
|
||||
delay: 1,
|
||||
})
|
||||
try {
|
||||
queue.request()
|
||||
queue.request()
|
||||
await waitFor(() => pending.length === 1)
|
||||
queue.request()
|
||||
pending[0]()
|
||||
await waitFor(() => pending.length === 2)
|
||||
assert.deepEqual(events, [])
|
||||
pending[1]()
|
||||
await waitFor(() => events.length === 1)
|
||||
assert.deepEqual(events, ['reload'])
|
||||
} finally {
|
||||
queue.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('failed latest generation reports an error without reloading', async () => {
|
||||
const events = []
|
||||
const queue = createRegenerationQueue({
|
||||
generate: () => Promise.reject(new Error('broken content')),
|
||||
onSuccess: () => events.push('reload'),
|
||||
onError: (error) => events.push(error.message),
|
||||
delay: 1,
|
||||
})
|
||||
try {
|
||||
queue.request()
|
||||
await waitFor(() => events.length === 1)
|
||||
assert.deepEqual(events, ['broken content'])
|
||||
} finally {
|
||||
queue.close()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { htmlFacts } from '../html-checks.js'
|
||||
|
||||
test('extracts decoded attributes regardless of quote style or order', () => {
|
||||
const facts = htmlFacts(`<!doctype html><meta content='width=device-width' name='viewport'>
|
||||
<a class='button' href='#a&b'>Go</a><h2 id='a&b'>Title</h2>
|
||||
<code class='extra language-js another'>code</code>`)
|
||||
assert.equal(facts.hasViewport, true)
|
||||
assert.deepEqual(facts.ids, ['a&b'])
|
||||
assert.deepEqual(facts.hrefs, ['#a&b'])
|
||||
assert.deepEqual(facts.languages, ['js'])
|
||||
})
|
||||
|
||||
test('ignores markup-like text in scripts and comments', () => {
|
||||
const facts = htmlFacts(`<script>const example = '<a href="/missing"><i id="fake"></i>'</script>
|
||||
<!-- <heading id="also-fake"></heading> -->
|
||||
<meta name='robots' content='nofollow, noindex'>`)
|
||||
assert.deepEqual(facts.ids, [])
|
||||
assert.deepEqual(facts.hrefs, [])
|
||||
assert.equal(facts.hasHeadingElement, false)
|
||||
assert.equal(facts.isDraft, true)
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { renderMarkdownPage } from '../markdown.js'
|
||||
|
||||
test('a page uses matching IDs in document and TOC order, including nested duplicates', () => {
|
||||
const page = renderMarkdownPage('# Title\n\n### Repeat\n\n## Repeat\n\n### Repeat\n\nA note^[with **emphasis**].\n\n```js\nconst x = 1\n```')
|
||||
const ids = [...page.html.matchAll(/<h[2-6] id="([^"]+)"/g)].map((match) => match[1])
|
||||
const links = [...page.toc.matchAll(/href="#([^"]+)"/g)].map((match) => match[1])
|
||||
assert.deepEqual(ids, ['repeat', 'repeat-2', 'repeat-3'])
|
||||
assert.deepEqual(links, ids)
|
||||
assert.match(page.html, /id="sidenote-1"/)
|
||||
assert.match(page.html, /<strong>emphasis<\/strong>/)
|
||||
assert.match(page.html, /class="language-js"/)
|
||||
assert.equal(page.excerpt, 'A note.')
|
||||
})
|
||||
|
||||
test('sidenote numbering resets for each page', () => {
|
||||
assert.match(renderMarkdownPage('# One\n\nA^[first]').html, /id="sidenote-1"/)
|
||||
assert.match(renderMarkdownPage('# Two\n\nB^[second]').html, /id="sidenote-1"/)
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { test } from 'node:test'
|
||||
import { createOutput, discardOutput, publishOutput } from '../output.js'
|
||||
import { readPages } from '../content.js'
|
||||
import { generatedRoot, siteUrl } from '../site.config.js'
|
||||
|
||||
test('headings have unique IDs and matching TOC links', async () => {
|
||||
const html = await readFile(join(generatedRoot, 'notes', 'steam', 'index.html'), 'utf8')
|
||||
const ids = [...html.matchAll(/<h[2-6] id="([^"]+)"/g)].map((match) => match[1])
|
||||
const links = [...html.matchAll(/<a href="#([^"]+)">/g)].map((match) => match[1])
|
||||
assert.ok(ids.length > 1)
|
||||
assert.equal(new Set(ids).size, ids.length)
|
||||
assert.deepEqual(links, ids)
|
||||
})
|
||||
|
||||
test('sidenotes render Markdown with paired, unique controls', async () => {
|
||||
const html = await readFile(join(generatedRoot, 'notes', 'lua', 'index.html'), 'utf8')
|
||||
const controls = [...html.matchAll(/<input type="checkbox" id="(sidenote-\d+)"/g)].map((match) => match[1])
|
||||
assert.deepEqual(controls, ['sidenote-1', 'sidenote-2'])
|
||||
for (const id of controls) assert.ok(html.includes(`<label for="${id}"`))
|
||||
assert.ok(html.includes('<strong>unstable</strong>'))
|
||||
})
|
||||
|
||||
test('production excludes hidden draft pages and feed links resolve', async () => {
|
||||
const pages = await readdir(join(generatedRoot, 'posts'))
|
||||
assert.ok(!pages.includes('rendering-lab'))
|
||||
const feed = await readFile(join(generatedRoot, 'feed.xml'), 'utf8')
|
||||
assert.ok(!feed.includes('rendering-lab'))
|
||||
for (const [, url] of feed.matchAll(/<guid isPermaLink="true">([^<]+)<\/guid>/g)) {
|
||||
assert.ok(url.startsWith(`${siteUrl}/`))
|
||||
const pathname = new URL(url).pathname
|
||||
await readFile(join(generatedRoot, pathname, 'index.html'))
|
||||
}
|
||||
})
|
||||
|
||||
test('content discovery excludes drafts and prefers a published page with the same slug', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'www-pages-test-'))
|
||||
try {
|
||||
await writeFile(join(directory, '.draft.md'), '# Draft\n')
|
||||
await writeFile(join(directory, '.same.md'), '# Old draft\n')
|
||||
await writeFile(join(directory, 'same.md'), '# Published\n')
|
||||
assert.deepEqual((await readPages(directory)).map(({ name }) => name), ['same'])
|
||||
const pages = await readPages(directory, { includeDrafts: true })
|
||||
assert.deepEqual(pages.map(({ name }) => name), ['draft', 'same'])
|
||||
assert.equal(pages.find(({ name }) => name === 'same').title, 'Published')
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('publishing replaces output, while a discarded generation preserves it', async () => {
|
||||
const parent = await mkdtemp(join(tmpdir(), 'www-output-test-'))
|
||||
const destination = join(parent, '.generated')
|
||||
try {
|
||||
const first = await createOutput(destination)
|
||||
await writeFile(join(first, 'index.html'), 'first')
|
||||
await publishOutput(first, destination)
|
||||
|
||||
const failed = await createOutput(destination)
|
||||
await writeFile(join(failed, 'index.html'), 'incomplete')
|
||||
await discardOutput(failed)
|
||||
assert.equal(await readFile(join(destination, 'index.html'), 'utf8'), 'first')
|
||||
|
||||
const missingStage = join(parent, 'missing-stage')
|
||||
await assert.rejects(publishOutput(missingStage, destination))
|
||||
assert.equal(await readFile(join(destination, 'index.html'), 'utf8'), 'first')
|
||||
|
||||
const second = await createOutput(destination)
|
||||
await writeFile(join(second, 'index.html'), 'second')
|
||||
await publishOutput(second, destination)
|
||||
assert.equal(await readFile(join(destination, 'index.html'), 'utf8'), 'second')
|
||||
} finally {
|
||||
await rm(parent, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user