55 */
66import { spawn , type Subprocess } from "bun" ;
77import { afterAll , beforeAll , describe , expect , test } from "bun:test" ;
8+ import { stat } from "node:fs/promises" ;
89import { join } from "node:path" ;
910
1011const TEST_PORT = 14_401 ;
1112const BASE_URL = process . env [ "TEST_URL" ] || `http://localhost:${ TEST_PORT } ` ;
13+ const BUILD_FRESHNESS_WINDOW_MS = 20 * 60 * 1000 ;
1214const CHANGELOG_CONTENT_TERM =
1315 process . env [ "DEPLOYMENT_CHANGELOG_CONTENT_TERM" ] ??
1416 "SUPERTOKENS_ACCESS_TOKEN_KEY" ;
@@ -30,23 +32,45 @@ async function waitForServer(maxAttempts = 30): Promise<void> {
3032 throw new Error ( `Server not ready after ${ maxAttempts } s` ) ;
3133}
3234
35+ async function hasFreshBuildArtifacts ( cwd : string ) : Promise < boolean > {
36+ const requiredArtifacts = [
37+ join ( cwd , ".output/server/wrangler.json" ) ,
38+ join ( cwd , ".output/server/index.mjs" ) ,
39+ join ( cwd , ".output/public/graphql/hive/index.html" ) ,
40+ ] ;
41+
42+ let artifactStats : Awaited < ReturnType < typeof stat > > [ ] ;
43+ try {
44+ artifactStats = await Promise . all (
45+ requiredArtifacts . map ( ( path ) => stat ( path ) ) ,
46+ ) ;
47+ } catch {
48+ return false ;
49+ }
50+
51+ const cutoffTime = Date . now ( ) - BUILD_FRESHNESS_WINDOW_MS ;
52+ return artifactStats . every ( ( artifact ) => artifact . mtimeMs >= cutoffTime ) ;
53+ }
54+
3355beforeAll ( async ( ) => {
3456 if ( process . env [ "TEST_URL" ] ) return ; // user-provided server
3557
3658 const cwd = join ( import . meta. dir , "../../.." ) ;
37- const build = spawn ( [ "bun" , "run" , "build" ] , {
38- cwd,
39- env : {
40- ...process . env ,
41- NODE_ENV : "production" ,
42- } ,
43- stderr : "inherit" ,
44- stdout : "inherit" ,
45- } ) ;
59+ if ( ! ( await hasFreshBuildArtifacts ( cwd ) ) ) {
60+ const build = spawn ( [ "bun" , "run" , "build" ] , {
61+ cwd,
62+ env : {
63+ ...process . env ,
64+ NODE_ENV : "production" ,
65+ } ,
66+ stderr : "inherit" ,
67+ stdout : "inherit" ,
68+ } ) ;
4669
47- const exitCode = await build . exited ;
48- if ( exitCode !== 0 ) {
49- throw new Error ( `Build failed with exit code ${ exitCode } ` ) ;
70+ const exitCode = await build . exited ;
71+ if ( exitCode !== 0 ) {
72+ throw new Error ( `Build failed with exit code ${ exitCode } ` ) ;
73+ }
5074 }
5175
5276 devServer = spawn (
@@ -218,6 +242,44 @@ describe("Accept header negotiation", () => {
218242 } ) ;
219243} ) ;
220244
245+ describe ( "prerendered HTML routing" , ( ) => {
246+ test ( "base-path route serves prerendered HTML without redirect" , async ( ) => {
247+ const res = await fetch ( `${ BASE_URL } /graphql/hive/docs/gateway` , {
248+ redirect : "manual" ,
249+ } ) ;
250+
251+ expect ( res . status ) . toBe ( 200 ) ;
252+ expect ( res . headers . get ( "content-type" ) ) . toContain ( "text/html" ) ;
253+ } ) ;
254+
255+ test ( "alias route serves prerendered HTML without redirect" , async ( ) => {
256+ const res = await fetch ( `${ BASE_URL } /docs/gateway` , {
257+ redirect : "manual" ,
258+ } ) ;
259+
260+ expect ( res . status ) . toBe ( 200 ) ;
261+ expect ( res . headers . get ( "content-type" ) ) . toContain ( "text/html" ) ;
262+ } ) ;
263+
264+ test ( "base-path trailing slash redirects to no-slash" , async ( ) => {
265+ const res = await fetch ( `${ BASE_URL } /graphql/hive/docs/gateway/` , {
266+ redirect : "manual" ,
267+ } ) ;
268+
269+ expect ( res . status ) . toBe ( 307 ) ;
270+ expect ( res . headers . get ( "location" ) ) . toBe ( "/graphql/hive/docs/gateway" ) ;
271+ } ) ;
272+
273+ test ( "alias trailing slash redirects to no-slash" , async ( ) => {
274+ const res = await fetch ( `${ BASE_URL } /docs/gateway/` , {
275+ redirect : "manual" ,
276+ } ) ;
277+
278+ expect ( res . status ) . toBe ( 307 ) ;
279+ expect ( res . headers . get ( "location" ) ) . toBe ( "/docs/gateway" ) ;
280+ } ) ;
281+ } ) ;
282+
221283describe ( "deployment changelog" , ( ) => {
222284 test ( "renders changelog html with mdx code-block chrome" , async ( ) => {
223285 const res = await fetch (
@@ -241,6 +303,26 @@ describe("deployment changelog", () => {
241303 } ) ;
242304} ) ;
243305
306+ describe ( "root routing" , ( ) => {
307+ test ( "aliased root serves landing page without redirect" , async ( ) => {
308+ const res = await fetch ( `${ BASE_URL } /` , {
309+ redirect : "manual" ,
310+ } ) ;
311+
312+ expect ( res . status ) . toBe ( 200 ) ;
313+ expect ( res . headers . get ( "content-type" ) ) . toContain ( "text/html" ) ;
314+ } ) ;
315+
316+ test ( "base-path trailing slash redirects to no-slash" , async ( ) => {
317+ const res = await fetch ( `${ BASE_URL } /graphql/hive/` , {
318+ redirect : "manual" ,
319+ } ) ;
320+
321+ expect ( res . status ) . toBe ( 307 ) ;
322+ expect ( res . headers . get ( "location" ) ) . toBe ( "/graphql/hive" ) ;
323+ } ) ;
324+ } ) ;
325+
244326describe ( "404 handling" , ( ) => {
245327 test ( ".mdx extension returns notFound for non-existent page" , async ( ) => {
246328 const res = await fetch ( `${ BASE_URL } /docs/non-existent-xyz.mdx` , {
0 commit comments