@@ -7,6 +7,7 @@ import { loadEnv, normalizePath } from 'vite';
77import { getRequest , setResponse } from '../../../exports/node/index.js' ;
88import { installPolyfills } from '../../../exports/node/polyfills.js' ;
99import { SVELTE_KIT_ASSETS } from '../../../constants.js' ;
10+ import { not_found } from '../utils.js' ;
1011
1112/** @typedef {import('http').IncomingMessage } Req */
1213/** @typedef {import('http').ServerResponse } Res */
@@ -21,6 +22,7 @@ export async function preview(vite, vite_config, svelte_config) {
2122 installPolyfills ( ) ;
2223
2324 const { paths } = svelte_config . kit ;
25+ const base = paths . base ;
2426 const assets = paths . assets ? SVELTE_KIT_ASSETS : paths . base ;
2527
2628 const protocol = vite_config . preview . https ? 'https' : 'http' ;
@@ -49,79 +51,131 @@ export async function preview(vite, vite_config, svelte_config) {
4951 } ) ;
5052
5153 return ( ) => {
52- // prerendered dependencies
54+ // Remove the base middleware. It screws with the URL.
55+ // It also only lets through requests beginning with the base path, so that requests beginning
56+ // with the assets URL never reach us. We could serve assets separately before the base
57+ // middleware, but we'd need that to occur after the compression and cors middlewares, so would
58+ // need to insert it manually into the stack, which would be at least as bad as doing this.
59+ for ( let i = vite . middlewares . stack . length - 1 ; i > 0 ; i -- ) {
60+ // @ts -expect-error using internals
61+ if ( vite . middlewares . stack [ i ] . handle . name === 'viteBaseMiddleware' ) {
62+ vite . middlewares . stack . splice ( i , 1 ) ;
63+ }
64+ }
65+
66+ // generated client assets and the contents of `static`
5367 vite . middlewares . use (
54- mutable ( join ( svelte_config . kit . outDir , 'output/prerendered/dependencies' ) )
68+ scoped (
69+ assets ,
70+ sirv ( join ( svelte_config . kit . outDir , 'output/client' ) , {
71+ setHeaders : ( res , pathname ) => {
72+ // only apply to immutable directory, not e.g. version.json
73+ if ( pathname . startsWith ( `/${ svelte_config . kit . appDir } /immutable` ) ) {
74+ res . setHeader ( 'cache-control' , 'public,max-age=31536000,immutable' ) ;
75+ }
76+ }
77+ } )
78+ )
5579 ) ;
5680
57- // prerendered pages (we can't just use sirv because we need to
58- // preserve the correct trailingSlash behaviour)
5981 vite . middlewares . use ( ( req , res , next ) => {
60- let if_none_match_value = req . headers [ 'if-none-match' ] ;
61-
62- if ( if_none_match_value ?. startsWith ( 'W/"' ) ) {
63- if_none_match_value = if_none_match_value . substring ( 2 ) ;
64- }
65-
66- if ( if_none_match_value === etag ) {
67- res . statusCode = 304 ;
82+ const original_url = /** @type {string } */ ( req . url ) ;
83+ const { pathname, search } = new URL ( original_url , 'http://dummy' ) ;
84+
85+ // if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
86+ // regardless of the `trailingSlash` route option
87+ if ( base . length > 1 && pathname === base ) {
88+ let location = base + '/' ;
89+ if ( search ) location += search ;
90+ res . writeHead ( 307 , {
91+ location
92+ } ) ;
6893 res . end ( ) ;
6994 return ;
7095 }
7196
72- const { pathname, search } = new URL ( /** @type {string } */ ( req . url ) , 'http://dummy' ) ;
97+ if ( pathname . startsWith ( base ) ) {
98+ next ( ) ;
99+ } else {
100+ res . statusCode = 404 ;
101+ not_found ( req , res , base ) ;
102+ }
103+ } ) ;
73104
74- let filename = normalizePath (
75- join ( svelte_config . kit . outDir , 'output/prerendered/pages' + pathname )
76- ) ;
77- let prerendered = is_file ( filename ) ;
105+ // prerendered dependencies
106+ vite . middlewares . use (
107+ scoped ( base , mutable ( join ( svelte_config . kit . outDir , 'output/prerendered/dependencies' ) ) )
108+ ) ;
78109
79- if ( ! prerendered ) {
80- const has_trailing_slash = pathname . endsWith ( '/' ) ;
81- const html_filename = `${ filename } ${ has_trailing_slash ? 'index.html' : '.html' } ` ;
110+ // prerendered pages (we can't just use sirv because we need to
111+ // preserve the correct trailingSlash behaviour)
112+ vite . middlewares . use (
113+ scoped ( base , ( req , res , next ) => {
114+ let if_none_match_value = req . headers [ 'if-none-match' ] ;
82115
83- /** @type {string | undefined } */
84- let redirect ;
116+ if ( if_none_match_value ?. startsWith ( 'W/"' ) ) {
117+ if_none_match_value = if_none_match_value . substring ( 2 ) ;
118+ }
85119
86- if ( is_file ( html_filename ) ) {
87- filename = html_filename ;
88- prerendered = true ;
89- } else if ( has_trailing_slash ) {
90- if ( is_file ( filename . slice ( 0 , - 1 ) + '.html' ) ) {
91- redirect = pathname . slice ( 0 , - 1 ) ;
92- }
93- } else if ( is_file ( filename + '/index.html' ) ) {
94- redirect = pathname + '/' ;
120+ if ( if_none_match_value === etag ) {
121+ res . statusCode = 304 ;
122+ res . end ( ) ;
123+ return ;
95124 }
96125
97- if ( redirect ) {
98- if ( search ) redirect += search ;
99- res . writeHead ( 307 , {
100- location : redirect
101- } ) ;
126+ const { pathname, search } = new URL ( /** @type {string } */ ( req . url ) , 'http://dummy' ) ;
127+
128+ let filename = normalizePath (
129+ join ( svelte_config . kit . outDir , 'output/prerendered/pages' + pathname )
130+ ) ;
131+ let prerendered = is_file ( filename ) ;
132+
133+ if ( ! prerendered ) {
134+ const has_trailing_slash = pathname . endsWith ( '/' ) ;
135+ const html_filename = `${ filename } ${ has_trailing_slash ? 'index.html' : '.html' } ` ;
136+
137+ /** @type {string | undefined } */
138+ let redirect ;
139+
140+ if ( is_file ( html_filename ) ) {
141+ filename = html_filename ;
142+ prerendered = true ;
143+ } else if ( has_trailing_slash ) {
144+ if ( is_file ( filename . slice ( 0 , - 1 ) + '.html' ) ) {
145+ redirect = pathname . slice ( 0 , - 1 ) ;
146+ }
147+ } else if ( is_file ( filename + '/index.html' ) ) {
148+ redirect = pathname + '/' ;
149+ }
102150
103- res . end ( ) ;
151+ if ( redirect ) {
152+ if ( search ) redirect += search ;
153+ res . writeHead ( 307 , {
154+ location : redirect
155+ } ) ;
104156
105- return ;
157+ res . end ( ) ;
158+
159+ return ;
160+ }
106161 }
107- }
108162
109- if ( prerendered ) {
110- res . writeHead ( 200 , {
111- 'content-type' : lookup ( pathname ) || 'text/html' ,
112- etag
113- } ) ;
163+ if ( prerendered ) {
164+ res . writeHead ( 200 , {
165+ 'content-type' : lookup ( pathname ) || 'text/html' ,
166+ etag
167+ } ) ;
114168
115- fs . createReadStream ( filename ) . pipe ( res ) ;
116- } else {
117- next ( ) ;
118- }
119- } ) ;
169+ fs . createReadStream ( filename ) . pipe ( res ) ;
170+ } else {
171+ next ( ) ;
172+ }
173+ } )
174+ ) ;
120175
121176 // SSR
122177 vite . middlewares . use ( async ( req , res ) => {
123178 const host = req . headers [ 'host' ] ;
124- req . url = req . originalUrl ;
125179
126180 const request = await getRequest ( {
127181 base : `${ protocol } ://${ host } ` ,
@@ -155,6 +209,28 @@ const mutable = (dir) =>
155209 } )
156210 : ( _req , _res , next ) => next ( ) ;
157211
212+ /**
213+ * @param {string } scope
214+ * @param {Handler } handler
215+ * @returns {Handler }
216+ */
217+ function scoped ( scope , handler ) {
218+ if ( scope === '' ) return handler ;
219+
220+ return ( req , res , next ) => {
221+ if ( req . url ?. startsWith ( scope ) ) {
222+ const original_url = req . url ;
223+ req . url = req . url . slice ( scope . length ) ;
224+ handler ( req , res , ( ) => {
225+ req . url = original_url ;
226+ next ( ) ;
227+ } ) ;
228+ } else {
229+ next ( ) ;
230+ }
231+ } ;
232+ }
233+
158234/** @param {string } path */
159235function is_file ( path ) {
160236 return fs . existsSync ( path ) && ! fs . statSync ( path ) . isDirectory ( ) ;
0 commit comments