Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions packages/ts-ssg/src/build/build-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
import { resolveThemeFileName, resolveThemeHref } from '@purestack/ts-style'
import { describe, expect, it } from 'vitest'
import { resolveBuildSiteConfig } from './build-config'

describe('resolveBuildSiteConfig', () => {
it.each([true, false])(
'keeps configured stylesheet paths stable for manifest reuse (publish %s)',
(publish) => {
const input = {
siteConfig: {
rootDir: process.cwd(),
style: {
fileName: 'site.css',
href: '/assets/site.css?custom=1#theme',
},
},
publish: { enabled: publish },
}
const first = resolveBuildSiteConfig(input)
const second = resolveBuildSiteConfig(input)
expect(first.style.fileName).toBe('site.css')
expect(second.style.fileName).toBe(first.style.fileName)
expect(first.style.href).toBe(input.siteConfig.style.href)
for (const theme of first.style.themes) {
expect(resolveThemeHref(first.style.href, theme)).toBe(
`/assets/${resolveThemeFileName(first.style.fileName, theme)}?custom=1#theme`,
)
}
},
)

it('applies publish overrides above site config values', () => {
const publishDir = `${process.cwd()}-publish`
const config = resolveBuildSiteConfig({
Expand Down
65 changes: 54 additions & 11 deletions packages/ts-ssg/src/build/incremental/incremental.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ async function fileExists(filePath: string) {
}
}

async function readThemeCss(outDir: string, theme: 'light' | 'dark') {
const assetsDir = path.join(outDir, 'assets')
const pattern =
theme === 'light'
? /^site\.[a-z0-9]+\.css$/
: /^site\.[a-z0-9]+\.dark\.css$/
const files = (await fs.readdir(assetsDir)).filter((file) =>
pattern.test(file),
)
expect(files).toHaveLength(1)
return fs.readFile(path.join(assetsDir, files[0]), 'utf8')
}

async function readScriptBundle(
outDir: string,
pageRelPath = path.join('hosts', 'index.html'),
Expand Down Expand Up @@ -60,6 +73,42 @@ describe('incremental builder', () => {
await logger?.close()
})

it.each([true, false])(
'builds pages linked to cache-keyed theme files (publish %s)',
async (publish) => {
await withTempDir(async (base) => {
const contentDir = path.join(base, 'content')
const publishDir = path.join(base, 'publish')
await fs.mkdir(contentDir)
await fs.writeFile(path.join(contentDir, 'index.md'), '# Home')
const builder = await createIncrementalBuilder({
siteConfig: {
rootDir: base,
contentDir,
publishDir,
outDir: publishDir,
basePath: '/docs',
style: { fileName: 'site.css', href: '/assets/site.css' },
},
publish: { enabled: publish },
})
await builder.buildAll('publish CSS cache keys')
const manifest = await readManifest(publishDir)
expect(manifest?.styles.outputs).toHaveLength(2)
const html = await fs.readFile(
path.join(publishDir, 'index.html'),
'utf8',
)
for (const output of manifest?.styles.outputs ?? []) {
const fileName = path.basename(output)
expect(fileName).toMatch(/^site\.[a-z0-9]+(?:\.dark)?\.css$/)
expect(html).toContain(`/docs/assets/${fileName}`)
expect((await fs.readFile(output, 'utf8')).length).toBeGreaterThan(0)
}
})
},
)

it('keeps style changes during CSS generation pending for the next request', async () => {
await withTempDir(async (base) => {
const contentDir = path.join(base, 'content')
Expand Down Expand Up @@ -101,9 +150,9 @@ describe('incremental builder', () => {
expect(writes).toBe(1)
await builder.preparePageAssets()
expect(writes).toBe(2)
expect(
await fs.readFile(path.join(outDir, 'assets', 'site.css'), 'utf8'),
).toContain('.concurrent-style')
expect(await readThemeCss(outDir, 'light')).toContain(
'.concurrent-style',
)
await builder.preparePageAssets()
expect(writes).toBe(2)
} finally {
Expand Down Expand Up @@ -374,14 +423,8 @@ describe('incremental builder', () => {

await builder.buildAll('test highlightjs styles')

const lightCss = await fs.readFile(
path.join(outDir, 'assets', 'site.css'),
'utf8',
)
const darkCss = await fs.readFile(
path.join(outDir, 'assets', 'site.dark.css'),
'utf8',
)
const lightCss = await readThemeCss(outDir, 'light')
const darkCss = await readThemeCss(outDir, 'dark')

expect(lightCss).not.toContain('pre.shiki.shiki-themes')
expect(darkCss).not.toContain('pre.shiki.shiki-themes')
Expand Down
5 changes: 4 additions & 1 deletion packages/ts-ssg/src/build/incremental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
} from '../manifest'
import { type BuildContext, resolveHeaderFooterHtml } from '../page'
import { buildPagefindIndex } from '../pagefind'
import { ScriptCacheKeyStore } from '../script-cache-key'
import { createScriptCacheKey, ScriptCacheKeyStore } from '../script-cache-key'
import type { BuildHooks, BuildInput, BuildResult } from '../site'
import { writeSitemap } from '../sitemap'
import { type WriteStylesResult, writeStyles } from '../styles'
Expand Down Expand Up @@ -110,6 +110,7 @@ async function createIncrementalRuntime(
: createEmptyManifest(config)
const scriptCacheKeys = new ScriptCacheKeyStore(manifest.assets)
const context: BuildContext = {
styleCacheKey: createScriptCacheKey(),
config,
// The content is prepared once, by the first build or the first change or
// request; see IncrementalRuntime.ensureContentReady.
Expand Down Expand Up @@ -295,6 +296,7 @@ class IncrementalRuntime {
}

buildAll = async (reason: string): Promise<BuildResult> => {
this.context.styleCacheKey = createScriptCacheKey()
const buildStartMs = Date.now()
const hooks = this.resolveBuildHooks()

Expand Down Expand Up @@ -421,6 +423,7 @@ class IncrementalRuntime {
{
outDir,
includeHljsTheme: isHighlightJsEnabled(this.config.mdx),
cacheKey: this.context.styleCacheKey,
},
style,
)
Expand Down
3 changes: 3 additions & 0 deletions packages/ts-ssg/src/build/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import { renderPage } from './renderer'
import type { BuildHooks } from './site'

export interface BuildContext {
/** Shared by generated stylesheet filenames and links for this build/session. */
styleCacheKey?: string
config: SiteConfig
contentRoutes: ContentRouteIndex
headerHtmlByDir?: Map<string, string>
Expand Down Expand Up @@ -399,6 +401,7 @@ async function renderPageShell(input: RenderPageShellInput): Promise<string> {
styleLinks: resolveThemeStyleLinks(
context.config.style.href,
context.config.style.themes,
context.styleCacheKey,
),
template,
templates: context.templates,
Expand Down
9 changes: 7 additions & 2 deletions packages/ts-ssg/src/build/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface WriteStylesResult {
}

export interface WriteStylesInput {
cacheKey?: string
outDir: string
includeHljsTheme?: boolean
}
Expand Down Expand Up @@ -50,7 +51,7 @@ export async function writeStyles(
!pretty,
includeHljsTheme,
)
const cssName = resolveThemeFileName(fileName, theme)
const cssName = resolveThemeFileName(fileName, theme, input.cacheKey)
const outPath = path.join(outDir, 'assets', cssName)
await ensureDir(outPath)
await fs.writeFile(outPath, css)
Expand All @@ -67,7 +68,11 @@ export async function writeStyles(
revision,
outPath:
lightOutPath ??
path.join(outDir, 'assets', resolveThemeFileName(fileName, 'light')),
path.join(
outDir,
'assets',
resolveThemeFileName(fileName, 'light', input.cacheKey),
),
outputs: resultPaths,
signature: hash.digest('hex'),
}
Expand Down
25 changes: 25 additions & 0 deletions packages/ts-style/src/themeAssets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { resolveThemeFileName, resolveThemeStyleLinks } from './themeAssets'

describe('cache-keyed theme assets', () => {
it('uses the same key in filenames and links while preserving URL suffixes', () => {
const links = resolveThemeStyleLinks(
'/assets/site.css?v=1#theme',
['light', 'dark'],
'build42',
)
for (const link of links) {
const fileName = resolveThemeFileName('site.css', link.theme, 'build42')
expect(link.href).toBe(`/assets/${fileName}?v=1#theme`)
}
expect(links.map((link) => link.href)).toEqual([
'/assets/site.build42.css?v=1#theme',
'/assets/site.build42.dark.css?v=1#theme',
])
})

it('keeps unkeyed paths and omitted stylesheets unchanged', () => {
expect(resolveThemeFileName('site.css', 'dark')).toBe('site.dark.css')
expect(resolveThemeStyleLinks('', ['light', 'dark'], 'build42')).toEqual([])
})
})
16 changes: 12 additions & 4 deletions packages/ts-style/src/themeAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,30 @@ export function resolveThemes(
export function resolveThemeFileName(
fileName: string,
theme: ThemeName,
cacheKey?: string,
): string {
const normalized = normalizeThemeName(theme)
const hasCss = fileName.toLowerCase().endsWith('.css')
const base = hasCss ? fileName.slice(0, -4) : fileName
const name = hasCss ? fileName.slice(0, -4) : fileName
const base = cacheKey ? `${name}.${cacheKey}` : name
const suffix = '.css'
if (normalized === 'light') {
return `${base}${suffix}`
}
return `${base}.${normalized}${suffix}`
}

export function resolveThemeHref(styleHref: string, theme: ThemeName): string {
export function resolveThemeHref(
styleHref: string,
theme: ThemeName,
cacheKey?: string,
): string {
if (!styleHref) return styleHref
const { path, query, hash } = splitHref(styleHref)
const normalized = normalizeThemeName(theme)
const hasCss = path.toLowerCase().endsWith('.css')
const base = hasCss ? path.slice(0, -4) : path
const name = hasCss ? path.slice(0, -4) : path
const base = cacheKey ? `${name}.${cacheKey}` : name
const suffix = '.css'
const themedPath =
normalized === 'light'
Expand All @@ -86,11 +93,12 @@ export function resolveThemeHref(styleHref: string, theme: ThemeName): string {
export function resolveThemeStyleLinks(
styleHref: string,
themes: ThemeName[],
cacheKey?: string,
): ThemeStylesheetLink[] {
if (!styleHref) return []
const ordered = orderThemes(themes)
return ordered.map((theme) => {
const href = resolveThemeHref(styleHref, theme)
const href = resolveThemeHref(styleHref, theme, cacheKey)
return {
theme,
href,
Expand Down
Loading