Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a685e8c
Render README build flows with Mermaid
bcomnes Sep 9, 2026
dfe80c4
Update build diagrams for subscriptions, manifests, and watch plans
bcomnes Sep 9, 2026
a8ca6eb
Make README a quick start and split detailed documentation
bcomnes Sep 6, 2026
cac604a
Add shared breadcrumbs to documentation pages
bcomnes Sep 6, 2026
f506cc4
Keep Mermaid diagrams working through nested documentation layouts
bcomnes Sep 9, 2026
e42d104
Print complete nested CLI diagnostics in build and watch mode
bcomnes Sep 9, 2026
63cd61a
Restore shared documentation navigation through global data
bcomnes Sep 9, 2026
0fcce5a
Use fragtml 0.0.10 and guard documentation rendering
bcomnes Sep 9, 2026
c3301ac
Simplify documentation titles and clarify page introductions
bcomnes Sep 9, 2026
c9c6bb1
Organize website globals and layouts under site
bcomnes Sep 9, 2026
8996c6c
Split documentation by layouts, data, generation, and settings
bcomnes Sep 9, 2026
adb3026
Add a responsive documentation shell with mobile navigation
bcomnes Sep 9, 2026
a78eb1b
Remove the site footer tagline
bcomnes Sep 9, 2026
e8ad7fb
Sync documentation URL and navigation with reading position
bcomnes Sep 9, 2026
b630063
Preserve existing smooth scrolling during docs navigation
bcomnes Sep 9, 2026
bc1e7f7
Polish site branding and add full-width frosted mobile navigation
bcomnes Sep 9, 2026
534133e
Restore explanatory structure in implementation diagrams
bcomnes Sep 9, 2026
c9c1bf4
Generate the footer copyright year during rendering
bcomnes Sep 9, 2026
b5d4054
Mark external website links consistently with decorative arrows
bcomnes Sep 9, 2026
32b0628
Remove legacy docs anchors and simplify navigation
bcomnes Sep 9, 2026
9527700
Re-arrange footer
bcomnes Sep 9, 2026
d25da37
Exclude repository-only content from the documentation website
bcomnes Sep 9, 2026
aa3e43f
Build watch contexts once and route diagnostics through the logger
bcomnes Sep 9, 2026
95273e8
Match copyright test to the updated footer order
bcomnes Sep 9, 2026
6efea14
Update sync and cpx2 to released watch fixes
bcomnes Sep 9, 2026
549059b
Align Node support with sync and test the minimum runtime
bcomnes Sep 9, 2026
382d92d
Keep CI on floating LTS and latest Node versions
bcomnes Sep 9, 2026
dfaf883
Focus documentation tests on behavioral regressions
bcomnes Sep 9, 2026
62ac000
Iteration session
bcomnes Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,7 @@ jobs:
- run: npm i
- run: npx playwright install --with-deps chromium
- name: Run tests
run: |
node_major=$(node -p "process.versions.node.split('.')[0]")
if [ "$node_major" -lt 23 ]; then
NODE_OPTIONS="--experimental-strip-types" npm test
else
npm test
fi
run: npm test
- name: Coveralls
uses: coverallsapp/github-action@v2
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
!.claude/hooks/
!.claude/hooks/worktree-hook.sh
!.claude/settings.json
.delta/*
node_modules
sandbox.js
.nyc_output
Expand Down
3,239 changes: 77 additions & 3,162 deletions README.md

Large diffs are not rendered by default.

34 changes: 28 additions & 6 deletions bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ const options = {
type: 'boolean',
help: 'watch and build the src folder without serving',
},
verbose: {
type: 'boolean',
help: 'show debug logs, including the build tree and individual copy operations',
},
serve: {
type: 'boolean',
help: 'build once and serve the destination directory without watching',
Expand Down Expand Up @@ -222,7 +226,7 @@ domstack eject actions:
opts.copy = copyPaths.map(p => resolve(cwd, p))
}

const logger = createDomStackLogger()
const logger = createDomStackLogger(argv['verbose'] ? 'debug' : 'info')
opts.logger = logger
const domStack = new DomStack(src, dest, opts)
/** @type {BsInstance | null} */
Expand Down Expand Up @@ -256,8 +260,9 @@ domstack eject actions:
if (!argv['watch'] && !argv['watch-only']) {
try {
const results = await domStack.build()
logger.info(tree(generateTreeData(cwd, src, dest, results)))
logger.debug(tree(generateTreeData(cwd, src, dest, results)))
logWarnings(logger, results?.warnings)
logger.info(`Built ${relative(cwd, src) || '.'} → ${relative(cwd, dest) || '.'}`)
logger.info('\nBuild Success!\n\n')
if (argv['serve']) {
buildServer = await createServer({
Expand All @@ -277,15 +282,15 @@ domstack eject actions:
}
}
if ('results' in err) delete err.results
logger.error(inspect(err, { depth: 999, colors: true }))
logger.error(formatDiagnostic(err, Boolean(process.stdout.isTTY)))
logger.error('\nBuild Failed!\n\n')
process.exit(1)
}
} else {
await domStack.watch({
serve: !argv['watch-only'],
onInitialBuild: (initialResults) => {
logger.info(tree(generateTreeData(cwd, src, dest, initialResults)))
logger.debug(tree(generateTreeData(cwd, src, dest, initialResults)))
logWarnings(logger, initialResults?.warnings)
},
})
Expand Down Expand Up @@ -315,12 +320,29 @@ function logWarnings (logger, warnings) {
if ('message' in warning) {
logger.warn(` ${warning.message}`)
} else {
logger.warn(inspect(warning, { depth: 999, colors: true }))
logger.warn(formatDiagnostic(warning, Boolean(process.stdout.isTTY)))
}
}
}

/**
* Keep nested causes, locations, and every diagnostic visible in CLI output.
* @param {unknown} value
* @param {boolean} colors
*/
function formatDiagnostic (value, colors) {
return inspect(value, {
depth: null,
maxArrayLength: null,
maxStringLength: null,
colors,
})
}

run().catch(err => {
console.error(new Error('Unhandled domstack error', { cause: err }))
console.error(formatDiagnostic(
new Error('Unhandled domstack error', { cause: err }),
Boolean(process.stderr.isTTY)
))
process.exit(1)
})
21 changes: 21 additions & 0 deletions browser-tests/docs-diagrams.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { resolve } from 'node:path'
import { expect, test, websiteOptions } from './support.js'

test.use({
siteSrc: resolve(import.meta.dirname, '..'),
siteOptions: websiteOptions,
})

test('the built documentation loads Mermaid and renders diagrams without errors', async ({ page, siteURL }) => {
const errors = []
page.on('pageerror', error => errors.push(error.message))
await page.goto(`${siteURL}/docs/implementation/`)
const diagrams = page.locator('.mermaid')
await expect(diagrams.first()).toBeVisible()
for (const diagram of await diagrams.all()) {
await expect(diagram.locator('svg')).toBeVisible()
// Mermaid may render an error SVG without throwing a JavaScript error.
await expect(diagram.locator('.error-icon')).toHaveCount(0)
}
expect(errors).toEqual([])
})
163 changes: 163 additions & 0 deletions browser-tests/docs-navigation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { resolve } from 'node:path'
import { expect, test, websiteOptions } from './support.js'

test.use({
siteSrc: resolve(import.meta.dirname, '..'),
siteOptions: websiteOptions,
})

test('an index section link navigates and selects the matching sidebar entry', async ({ page, siteURL }) => {
await page.setViewportSize({ width: 1500, height: 900 })
await page.goto(`${siteURL}/docs/`)
await page.locator('.docs-index a[href="layouts/#declaring-nested-layouts"]').click()
await expect(page).toHaveURL(`${siteURL}/docs/layouts/#declaring-nested-layouts`)
const nav = page.getByRole('navigation', { name: 'Documentation', exact: true })
await expect(nav.locator('a[aria-current="page"]')).toHaveText('Layouts')
await expect(nav.locator('a[aria-current="location"]')).toHaveText('Declaring nested layouts')
})

test('disclosure arrows animate around a stable center and respect reduced motion', async ({ page, siteURL }) => {
await page.emulateMedia({ reducedMotion: 'no-preference' })
await page.setViewportSize({ width: 1500, height: 900 })
await page.goto(`${siteURL}/docs/layouts/`)
const summary = page.locator('.docs-navigation summary').filter({ has: page.getByRole('link', { name: 'CLI', exact: true }) })
const arrowStyle = () => summary.locator('.docs-navigation-chevron').evaluate(element => {
const style = getComputedStyle(element)
return {
width: parseFloat(style.width),
height: parseFloat(style.height),
origin: style.transformOrigin,
transform: style.transform,
duration: style.transitionDuration,

}
})
const closed = await arrowStyle()
expect(closed.duration).toBe('0.2s')
expect(closed.width).toBe(16)
expect(closed.height).toBe(16)
const bounds = await summary.boundingBox()
await summary.click({ position: { x: bounds.width - 16, y: bounds.height / 2 } })
await expect(summary.locator('..')).toHaveAttribute('open', '')
await expect.poll(async () => (await arrowStyle()).transform).not.toBe(closed.transform)
const opened = await arrowStyle()
expect(opened.width).toBe(closed.width)
expect(opened.height).toBe(closed.height)
expect(opened.origin).toBe(closed.origin)
expect(opened.transform).not.toBe(closed.transform)

await page.emulateMedia({ reducedMotion: 'reduce' })
await expect.poll(async () => (await arrowStyle()).duration).toBe('0s')
})

test('global bundles and cookbook subpages are linked from the documentation', async ({ page, siteURL }) => {
await page.setViewportSize({ width: 1500, height: 900 })
await page.goto(`${siteURL}/docs/`)
await page.locator('.docs-index a[href="global-bundles/"]').click()
await expect(page.locator('.docs-content > h1')).toHaveText('Global bundles')
await expect(page.locator('.docs-navigation a[aria-current="page"]')).toHaveText('Global bundles')

await page.goto(`${siteURL}/docs/cookbook/`)
const recipes = await page.locator('.docs-content > ul > li > a').evaluateAll(links => links.map(link => ({
href: link.href,
title: link.textContent,
})))
expect(recipes).toHaveLength(4)
for (const recipe of recipes) {
const response = await page.goto(recipe.href)
expect(response.status()).toBe(200)
await expect(page.locator('.docs-content > h1')).toHaveText(recipe.title)
await expect(page.locator('.docs-navigation a[aria-current="page"]')).toHaveText(recipe.title)
await expect(page.locator('.docs-navigation nav > ul > li > details[open] > summary')).toHaveText('Recipes')
await expect(page.locator('.docs-navigation nav > ul > li > details[open] > ul > li > a[aria-current="page"]')).toHaveText(recipe.title)
await page.getByRole('link', { name: 'All recipes', exact: true }).click()
await expect(page).toHaveURL(`${siteURL}/docs/cookbook/`)
}
})

test('mobile navigation manages focus, section links, and return to desktop', async ({ page, siteURL }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${siteURL}/docs/layouts/`)
const toggle = page.getByRole('button', { name: 'Open documentation menu' })
const menu = page.getByRole('dialog', { name: 'Documentation', exact: true })
const close = menu.getByRole('button', { name: 'Close documentation menu' })
await toggle.focus()
await page.keyboard.press('Enter')
await expect(menu).toBeVisible()
await expect(toggle).toHaveAttribute('aria-expanded', 'true')
await expect(close).toBeFocused()
// Opening the modal must prevent focus from escaping into the page.
await toggle.evaluate(el => el.focus())
await expect(close).toBeFocused()
await page.keyboard.press('Escape')
await expect(menu).toBeHidden()
await expect(toggle).toBeFocused()
await expect(toggle).toHaveAttribute('aria-expanded', 'false')

await toggle.click()
await menu.getByRole('link', { name: 'Layout module exports', exact: true }).click()
await expect(menu).toBeHidden()
await expect(page).toHaveURL(`${siteURL}/docs/layouts/#layout-module-exports`)
await expect(page.locator('#layout-module-exports')).toBeFocused()
expect(await page.locator('html').evaluate(el => getComputedStyle(el).overflow)).not.toBe('hidden')

// Resizing an open menu must restore navigation without leaving a modal or
// scroll lock behind. The restored links must still work.
await toggle.click()
await page.setViewportSize({ width: 1500, height: 900 })
await expect(menu).toBeHidden()
await expect(toggle).toBeHidden()
const nav = page.getByRole('navigation', { name: 'Documentation', exact: true })
await expect(nav).toBeVisible()
await expect(nav).toHaveCount(1)
expect(await page.locator('html').evaluate(el => getComputedStyle(el).overflow)).not.toBe('hidden')
await nav.getByRole('link', { name: 'CLI', exact: true }).click()
await expect(page).toHaveURL(`${siteURL}/docs/cli/`)
})

test('migration links support pointer navigation', async ({ page, siteURL }) => {
await page.goto(`${siteURL}/docs/migrations/`)
for (const version of ['v12', 'v11']) {
await page.locator('.docs-content > ul').getByRole('link', { name: `${version} migration`, exact: true }).click()
await expect(page).toHaveURL(`${siteURL}/docs/migrations/${version}-migration.html`)
await page.getByRole('link', { name: 'All migrations', exact: true }).click()
await expect(page).toHaveURL(`${siteURL}/docs/migrations/`)
}
})

test.describe('without JavaScript', () => {
test.use({ javaScriptEnabled: false })

test('migration guides are nested and linked through the migrations page', async ({ page, siteURL }) => {
await page.setViewportSize({ width: 1500, height: 900 })
await page.goto(`${siteURL}/docs/`)
await page.locator('.docs-index > ul > li > a[href="migrations/"]').focus()
await page.keyboard.press('Enter')
await expect(page.locator('.docs-content > h1')).toHaveText('Migrations')
for (const version of ['v12', 'v11']) {
await page.locator('.docs-content > ul').getByRole('link', { name: `${version} migration`, exact: true }).focus()
await page.keyboard.press('Enter')
await expect(page).toHaveURL(`${siteURL}/docs/migrations/${version}-migration.html`)
await expect(page.locator('.docs-content > h1')).toHaveText(`${version} migration`)
await expect(page.locator('.docs-navigation a[aria-current="page"]')).toHaveText(`${version} migration`)
await expect(page.locator('.docs-navigation nav details[open] > summary')).toHaveText('Migrations')
await expect(page.locator('.docs-breadcrumb').getByRole('link', { name: 'migrations', exact: true })).toHaveAttribute('href', './')
await page.getByRole('link', { name: 'All migrations', exact: true }).focus()
await page.keyboard.press('Enter')
await expect(page).toHaveURL(`${siteURL}/docs/migrations/`)
}
})

test('the server-rendered index and mobile navigation remain usable', async ({ page, siteURL }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${siteURL}/docs/`)
await page.locator('.docs-index a[href="layouts/#declaring-nested-layouts"]').click()
await expect(page).toHaveURL(`${siteURL}/docs/layouts/#declaring-nested-layouts`)
await expect(page.getByRole('button', { name: 'Open documentation menu' })).toBeHidden()
const nav = page.locator('.docs-navigation')
await expect(nav).toBeVisible()
await nav.getByRole('link', { name: 'CLI', exact: true }).focus()
await page.keyboard.press('Enter')
await expect(page).toHaveURL(`${siteURL}/docs/cli/`)
})
})
Loading
Loading