Skip to content
Closed
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
9 changes: 9 additions & 0 deletions apps/landing/check-prerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { fileURLToPath } from 'node:url'

import { EXPECTED_EXPORTED_ROUTES } from '../../e2e/exported-routes'
import { verifyPrerender } from './verify-prerender'

verifyPrerender(
fileURLToPath(new URL('./dist/client/', import.meta.url)),
EXPECTED_EXPORTED_ROUTES,
)
2 changes: 1 addition & 1 deletion apps/landing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"dev": "node ./script.js && vinext dev",
"search": "node ./script.js",
"build": "node ./script.js && vinext build",
"build": "node ./script.js && vinext build && bun ./check-prerender.ts",
"build:next": "node ./script.js && LANDING_BUILD_MODE=next next build",
"start": "npx serve ./dist/client",
"lint": "eslint"
Expand Down
46 changes: 46 additions & 0 deletions apps/landing/verify-prerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { statSync } from 'node:fs'
import { resolve } from 'node:path'

import {
getOutputPath,
getRscOutputPath,
} from 'vinext/internal/utils/prerender-output-paths'

export class PrerenderArtifactError extends Error {
constructor(
readonly route: string,
readonly path: string,
readonly reason: 'missing' | 'empty' | 'not-file',
) {
super(`Prerender artifact ${reason}: route ${route}, artifact ${path}`)
this.name = 'PrerenderArtifactError'
}
}

function requireArtifact(route: string, path: string): void {
const stat = statSync(path, { throwIfNoEntry: false })
if (!stat) throw new PrerenderArtifactError(route, path, 'missing')
if (!stat.isFile()) throw new PrerenderArtifactError(route, path, 'not-file')
if (stat.size === 0) throw new PrerenderArtifactError(route, path, 'empty')
}

export function verifyPrerender(
outputDirectory: string,
expectedRoutes: readonly string[],
): void {
// Match landing's output: 'export', trailingSlash: false, empty basePath.
for (const route of expectedRoutes) {
requireArtifact(
route,
resolve(outputDirectory, getOutputPath(route, false)),
)
requireArtifact(
route,
resolve(
outputDirectory,
getRscOutputPath(route, { mode: 'export', trailingSlash: false }),
),
)
}
requireArtifact('/404', resolve(outputDirectory, '404.html'))
}
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@
"apps/*",
"bindings/*",
"benchmark/*"
]
],
"patchedDependencies": {
"vinext@1.2.1": "patches/vinext@1.2.1.patch"
}
}
111 changes: 111 additions & 0 deletions packages/vite-plugin/src/__tests__/landing-prerender.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

import { describe, expect, it } from 'bun:test'

import {
PrerenderArtifactError,
verifyPrerender,
} from '../../../../apps/landing/verify-prerender'

const routes = ['/', '/nested/leaf'] as const
const artifacts = [
['/', 'index.html'],
['/', 'index.txt'],
['/nested/leaf', 'nested/leaf.html'],
['/nested/leaf', 'nested/leaf.txt'],
['/404', '404.html'],
] as const

function temporaryOutput() {
const root = mkdtempSync(join(tmpdir(), 'landing-prerender-'))
return {
root,
[Symbol.dispose]() {
rmSync(root, { recursive: true, force: true })
},
}
}

function writeCompleteOutput(root: string): void {
mkdirSync(join(root, 'nested'))
// Independent fixture paths, not generated by Vinext or the verifier.
writeFileSync(join(root, 'index.html'), '<html>home</html>')
writeFileSync(join(root, 'index.txt'), 'home Flight')
writeFileSync(join(root, 'nested/leaf.html'), '<html>leaf</html>')
writeFileSync(join(root, 'nested/leaf.txt'), 'leaf Flight')
writeFileSync(join(root, '404.html'), '<html>not found</html>')
}

it('accepts complete route pairs when the 404 has HTML only', () => {
// Given
using output = temporaryOutput()
writeCompleteOutput(output.root)
// When / Then
expect(() => verifyPrerender(output.root, routes)).not.toThrow()
})

describe.each(artifacts)('route %s artifact %s', (route, artifact) => {
it('rejects the artifact when it is missing', () => {
// Given
using output = temporaryOutput()
writeCompleteOutput(output.root)
const path = join(output.root, artifact)
rmSync(path)
// When / Then
expect(() => verifyPrerender(output.root, routes)).toThrow(
expect.objectContaining({
constructor: PrerenderArtifactError,
route,
path,
reason: 'missing',
}),
)
})

it('rejects the artifact when it is empty', () => {
// Given
using output = temporaryOutput()
writeCompleteOutput(output.root)
const path = join(output.root, artifact)
writeFileSync(path, '')
// When / Then
expect(() => verifyPrerender(output.root, routes)).toThrow(
expect.objectContaining({
constructor: PrerenderArtifactError,
route,
path,
reason: 'empty',
}),
)
})

it('rejects the artifact when it is a directory', () => {
// Given
using output = temporaryOutput()
writeCompleteOutput(output.root)
const path = join(output.root, artifact)
rmSync(path)
mkdirSync(path)
// When / Then
expect(() => verifyPrerender(output.root, routes)).toThrow(
expect.objectContaining({
constructor: PrerenderArtifactError,
route,
path,
reason: 'not-file',
}),
)
})
})

it('propagates native filesystem errors when the path contains a null byte', () => {
// Given
using output = temporaryOutput()
const path = join(output.root, '\0')
// When / Then
expect(() => verifyPrerender(path, routes)).toThrow(
expect.objectContaining({ code: 'ERR_INVALID_ARG_VALUE' }),
)
})
Loading
Loading