diff --git a/.changeset/four-pandas-push.md b/.changeset/four-pandas-push.md new file mode 100644 index 000000000000..acc2609b0cd4 --- /dev/null +++ b/.changeset/four-pandas-push.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/adapter-node': patch +--- + +[breaking] set Vite's `publicDir` and `base` options diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index ec1d53eeccfe..4dbd7d1bd9ad 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -50,7 +50,6 @@ export default function (opts = {}) { if (precompress) { builder.log.minor('Compressing assets'); await compress(`${out}/client`); - await compress(`${out}/static`); await compress(`${out}/prerendered`); } } diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index b2e52c86aebc..d0b5dad7ffdb 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -122,10 +122,7 @@ export function create_builder({ config, build_data, prerendered, log }) { }, writeClient(dest) { - return [ - ...copy(`${config.kit.outDir}/output/client`, dest), - ...copy(config.kit.files.assets, dest) - ]; + return [...copy(`${config.kit.outDir}/output/client`, dest)]; }, writePrerendered(dest, { fallback } = {}) { diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js index 0af84ddec6c5..2f2c82f4a78f 100644 --- a/packages/kit/src/core/adapt/builder.spec.js +++ b/packages/kit/src/core/adapt/builder.spec.js @@ -43,13 +43,7 @@ test('copy files', () => { builder.writeClient(dest); assert.equal( - [ - ...glob('**', { - cwd: /** @type {import('types').ValidatedConfig} */ (mocked).kit.files.assets, - dot: true - }), - ...glob('**', { cwd: `${outDir}/output/client`, dot: true }) - ], + glob('**', { cwd: `${outDir}/output/client`, dot: true }), glob('**', { cwd: dest, dot: true }) ); diff --git a/packages/kit/src/core/prerender/prerender.js b/packages/kit/src/core/prerender/prerender.js index 5995984c2861..a16cdbc83140 100644 --- a/packages/kit/src/core/prerender/prerender.js +++ b/packages/kit/src/core/prerender/prerender.js @@ -103,24 +103,22 @@ export async function prerender({ config, entries, files, log }) { /** * @param {string | null} referrer * @param {string} decoded - * @param {string} [encoded] */ - function enqueue(referrer, decoded, encoded) { + function enqueue(referrer, decoded) { if (seen.has(decoded)) return; seen.add(decoded); const file = decoded.slice(config.paths.base.length + 1); if (files.has(file)) return; - return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer)); + return q.add(() => visit(decoded, referrer)); } /** * @param {string} decoded - * @param {string} encoded * @param {string?} referrer */ - async function visit(decoded, encoded, referrer) { + async function visit(decoded, referrer) { if (!decoded.startsWith(config.paths.base)) { error({ status: 404, path: decoded, referrer, referenceType: 'linked' }); return; @@ -129,7 +127,8 @@ export async function prerender({ config, entries, files, log }) { /** @type {Map} */ const dependencies = new Map(); - const response = await server.respond(new Request(`http://sveltekit-prerender${encoded}`), { + const path = encodeURI(decoded.slice(config.paths.base.length)); + const response = await server.respond(new Request(`http://sveltekit-prerender${path}`), { getClientAddress, prerendering: { dependencies @@ -138,6 +137,8 @@ export async function prerender({ config, entries, files, log }) { const body = Buffer.from(await response.arrayBuffer()); + const encoded = encodeURI(decoded); + save('pages', response, body, decoded, encoded, referrer, 'linked'); for (const [dependency_path, result] of dependencies) { @@ -171,7 +172,7 @@ export async function prerender({ config, entries, files, log }) { // TODO warn that query strings have no effect on statically-exported pages } - enqueue(decoded, decodeURI(pathname), pathname); + enqueue(decoded, decodeURI(pathname)); } } } @@ -201,7 +202,7 @@ export async function prerender({ config, entries, files, log }) { if (location) { const resolved = resolve(encoded, location); if (is_root_relative(resolved)) { - enqueue(decoded, decodeURI(resolved), resolved); + enqueue(decoded, decodeURI(resolved)); } if (!response.headers.get('x-sveltekit-normalize')) { diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index d149441a0cfb..a1a69c8196fd 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -57,13 +57,6 @@ export async function respond(request, options, state) { /** @type {Record} */ let params = {}; - if (options.paths.base && !state.prerendering?.fallback) { - if (!decoded.startsWith(options.paths.base)) { - return new Response('Not found', { status: 404 }); - } - decoded = decoded.slice(options.paths.base.length) || '/'; - } - const is_data_request = decoded.endsWith(DATA_SUFFIX); if (is_data_request) { @@ -93,13 +86,14 @@ export async function respond(request, options, state) { const normalized = normalize_path(url.pathname, options.trailing_slash); if (normalized !== url.pathname && !state.prerendering?.fallback) { + const path = options.paths.base + normalized; return new Response(undefined, { status: 301, headers: { 'x-sveltekit-normalize': '1', location: // ensure paths starting with '//' are not treated as protocol-relative - (normalized.startsWith('//') ? url.origin + normalized : normalized) + + (normalized.startsWith('//') ? url.origin + path : path) + (url.search === '?' ? '' : url.search) } }); diff --git a/packages/kit/src/vite/build/utils.js b/packages/kit/src/vite/build/utils.js index a0acb8771215..7636072d089b 100644 --- a/packages/kit/src/vite/build/utils.js +++ b/packages/kit/src/vite/build/utils.js @@ -116,9 +116,7 @@ export const get_default_config = function ({ config, input, ssr, outDir }) { __SVELTEKIT_APP_VERSION_FILE__: JSON.stringify(`${config.kit.appDir}/version.json`), __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: JSON.stringify(config.kit.version.pollInterval) }, - // prevent Vite copying the contents of `config.kit.files.assets`, - // if it happens to be 'public' instead of 'static' - publicDir: false, + publicDir: ssr ? false : config.kit.files.assets, resolve: { alias: get_aliases(config.kit) }, diff --git a/packages/kit/src/vite/dev/index.js b/packages/kit/src/vite/dev/index.js index b118afc23070..800678eda7d9 100644 --- a/packages/kit/src/vite/dev/index.js +++ b/packages/kit/src/vite/dev/index.js @@ -1,7 +1,6 @@ import fs from 'fs'; import colors from 'kleur'; import path from 'path'; -import sirv from 'sirv'; import { URL } from 'url'; import { getRequest, setResponse } from '../../node/index.js'; import { installPolyfills } from '../../node/polyfills.js'; @@ -173,12 +172,6 @@ export async function dev(vite, vite_config, svelte_config) { } const assets = svelte_config.kit.paths.assets ? SVELTE_KIT_ASSETS : svelte_config.kit.paths.base; - const asset_server = sirv(svelte_config.kit.files.assets, { - dev: true, - etag: true, - maxAge: 0, - extensions: [] - }); return () => { const serve_static_middleware = vite.middlewares.stack.find( @@ -197,20 +190,6 @@ export async function dev(vite, vite_config, svelte_config) { }`; const decoded = decodeURI(new URL(base + req.url).pathname); - - if (decoded.startsWith(assets)) { - const pathname = decoded.slice(assets.length); - const file = svelte_config.kit.files.assets + pathname; - - if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) { - if (has_correct_case(file, svelte_config.kit.files.assets)) { - req.url = encodeURI(pathname); // don't need query/hash - asset_server(req, res); - return; - } - } - } - const file = posixify(path.resolve(decoded.slice(1))); const is_file = fs.existsSync(file) && !fs.statSync(file).isDirectory(); const allowed = @@ -223,13 +202,6 @@ export async function dev(vite, vite_config, svelte_config) { return; } - if (!decoded.startsWith(svelte_config.kit.paths.base)) { - return not_found( - res, - `Not found (did you mean ${svelte_config.kit.paths.base + req.url}?)` - ); - } - /** @type {Partial} */ const user_hooks = resolve_entry(svelte_config.kit.files.hooks) ? await vite.ssrLoadModule(`/${svelte_config.kit.files.hooks}`) @@ -383,12 +355,6 @@ export async function dev(vite, vite_config, svelte_config) { }; } -/** @param {import('http').ServerResponse} res */ -function not_found(res, message = 'Not found') { - res.statusCode = 404; - res.end(message); -} - /** * @param {import('connect').Server} server */ @@ -444,24 +410,3 @@ async function find_deps(vite, node, deps) { await Promise.all(branches); } - -/** - * Determine if a file is being requested with the correct case, - * to ensure consistent behaviour between dev and prod and across - * operating systems. Note that we can't use realpath here, - * because we don't want to follow symlinks - * @param {string} file - * @param {string} assets - * @returns {boolean} - */ -function has_correct_case(file, assets) { - if (file === assets) return true; - - const parent = path.dirname(file); - - if (fs.readdirSync(parent).includes(path.basename(file))) { - return has_correct_case(parent, assets); - } - - return false; -} diff --git a/packages/kit/src/vite/index.js b/packages/kit/src/vite/index.js index 08dc043cf598..af22c0491c4a 100644 --- a/packages/kit/src/vite/index.js +++ b/packages/kit/src/vite/index.js @@ -197,7 +197,7 @@ function kit() { /** @type {import('vite').UserConfig} */ const result = { appType: 'custom', - base: '/', + base: `${svelte_config.kit.paths.base}/`, build: { rollupOptions: { // Vite dependency crawler needs an explicit JS entry point @@ -208,6 +208,7 @@ function kit() { define: { __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0' }, + publicDir: svelte_config.kit.files.assets, resolve: { alias: get_aliases(svelte_config.kit) }, diff --git a/packages/kit/src/vite/preview/index.js b/packages/kit/src/vite/preview/index.js index 078e6146ed3a..03ae86cd0764 100644 --- a/packages/kit/src/vite/preview/index.js +++ b/packages/kit/src/vite/preview/index.js @@ -44,9 +44,6 @@ export async function preview(vite, config, protocol) { const server = new Server(manifest); return () => { - // files in `static` - vite.middlewares.use(scoped(assets, mutable(config.kit.files.assets))); - // immutable generated client assets vite.middlewares.use( scoped( @@ -70,7 +67,11 @@ export async function preview(vite, config, protocol) { next(); } else { res.statusCode = 404; - res.end(`Not found (did you mean ${base + pathname}?)`); + res.end( + `The server is configured with a public base URL of ${base}/ - did you mean to visit ${base + pathname} instead?` + ); } }); @@ -122,29 +123,31 @@ export async function preview(vite, config, protocol) { ); // SSR - vite.middlewares.use(async (req, res) => { - const host = req.headers['host']; + vite.middlewares.use( + scoped(base, async (req, res) => { + const host = req.headers['host']; - let request; + let request; - try { - request = await getRequest(`${protocol}://${host}`, req); - } catch (/** @type {any} */ err) { - res.statusCode = err.status || 400; - return res.end(err.reason || 'Invalid request body'); - } + try { + request = await getRequest(`${protocol}://${host}`, req); + } catch (/** @type {any} */ err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } - setResponse( - res, - await server.respond(request, { - getClientAddress: () => { - const { remoteAddress } = req.socket; - if (remoteAddress) return remoteAddress; - throw new Error('Could not determine clientAddress'); - } - }) - ); - }); + setResponse( + res, + await server.respond(request, { + getClientAddress: () => { + const { remoteAddress } = req.socket; + if (remoteAddress) return remoteAddress; + throw new Error('Could not determine clientAddress'); + } + }) + ); + }) + ); }; } diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index a4402c4c0c00..394aaf6839b8 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -473,11 +473,6 @@ test.describe('Static files', () => { expect(await r2.json()).toEqual({ works: true }); }); - test('Filenames are case-sensitive', async ({ request }) => { - const response = await request.get('/static.JSON'); - expect(response.status()).toBe(404); - }); - test('Serves symlinked asset', async ({ request }) => { const response = await request.get('/symlink-from/hello.txt'); expect(response.status()).toBe(200); diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js index 9ca9a0eb5bf0..fdea1e23026f 100644 --- a/packages/kit/test/apps/options-2/test/test.js +++ b/packages/kit/test/apps/options-2/test/test.js @@ -7,7 +7,7 @@ test.describe.configure({ mode: 'parallel' }); test.describe('paths.base', () => { test('serves /basepath', async ({ page }) => { - await page.goto('/basepath'); + await page.goto('/basepath/'); expect(await page.textContent('h1')).toBe('Hello'); }); @@ -28,7 +28,7 @@ test.describe('Service worker', () => { }); test('does not register /basepath/service-worker.js', async ({ page }) => { - await page.goto('/basepath'); + await page.goto('/basepath/'); expect(await page.content()).not.toMatch(/navigator\.serviceWorker/); }); }); diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index c7af4695c15b..565e43262dfe 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -7,9 +7,11 @@ test.describe.configure({ mode: 'parallel' }); test.describe('base path', () => { test('serves a useful 404 when visiting unprefixed path', async ({ request }) => { - const response = await request.get('/'); + const response = await request.get('/slash/', { headers: { Accept: 'text/html' } }); expect(response.status()).toBe(404); - expect(await response.text()).toBe('Not found (did you mean /path-base/?)'); + expect(await response.text()).toBe( + 'The server is configured with a public base URL of /path-base/ - did you mean to visit /path-base/slash/ instead?' + ); }); test('serves /', async ({ page, javaScriptEnabled }) => {