diff --git a/.changeset/wise-berries-flash.md b/.changeset/wise-berries-flash.md new file mode 100644 index 000000000000..8cb7218931d9 --- /dev/null +++ b/.changeset/wise-berries-flash.md @@ -0,0 +1,10 @@ +--- +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-cloudflare-workers': patch +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-vercel': patch +'@sveltejs/kit': patch +--- + +only serve `_app/immutable` with immutable cache header, not `_app/version.json` diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index 2d2a5d32c0cf..1064a1547763 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -23,11 +23,15 @@ export default { const res = await get_asset_from_kv(req, env, context); if (is_error(res.status)) return res; + const cache_control = url.pathname.startsWith(prefix + 'immutable/') + ? 'public, immutable, max-age=31536000' + : 'no-cache'; + return new Response(res.body, { headers: { - // include original cache headers, minus cache-control which + // include original headers, minus cache-control which // is overridden, and etag which is no longer useful - 'cache-control': 'public, immutable, max-age=31536000', + 'cache-control': cache_control, 'content-type': res.headers.get('content-type'), 'x-robots-tag': 'noindex' } diff --git a/packages/adapter-cloudflare/src/worker.js b/packages/adapter-cloudflare/src/worker.js index ad7c4643034d..4a53b71ea0ee 100644 --- a/packages/adapter-cloudflare/src/worker.js +++ b/packages/adapter-cloudflare/src/worker.js @@ -20,11 +20,15 @@ const worker = { if (pathname.startsWith(prefix)) { res = await env.ASSETS.fetch(req); + const cache_control = pathname.startsWith(prefix + 'immutable/') + ? 'public, immutable, max-age=31536000' + : 'no-cache'; + res = new Response(res.body, { headers: { - // include original cache headers, minus cache-control which + // include original headers, minus cache-control which // is overridden, and etag which is no longer useful - 'cache-control': 'public, immutable, max-age=31536000', + 'cache-control': cache_control, 'content-type': res.headers.get('content-type'), 'x-robots-tag': 'noindex' } diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 89fa7305cc4e..774db538a5eb 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -64,7 +64,7 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { builder.copy('_headers', headers_file); appendFileSync( headers_file, - `\n\n/${builder.config.kit.appDir}/*\n cache-control: public\n cache-control: immutable\n cache-control: max-age=31536000\n` + `\n\n/${builder.config.kit.appDir}/immutable/*\n cache-control: public\n cache-control: immutable\n cache-control: max-age=31536000\n` ); // for esbuild, use ESM diff --git a/packages/adapter-netlify/src/edge.js b/packages/adapter-netlify/src/edge.js index 89d2eb7e58ec..533d57eb2cc9 100644 --- a/packages/adapter-netlify/src/edge.js +++ b/packages/adapter-netlify/src/edge.js @@ -12,6 +12,7 @@ const prefix = `/${manifest.appDir}/`; export default function handler(request, context) { if (is_static_file(request)) { // Static files can skip the handler + // TODO can we serve _app/immutable files with an immutable cache header? return; } @@ -33,6 +34,7 @@ function is_static_file(request) { if (url.pathname.startsWith(prefix)) { return true; } + // prerendered pages and index.html files const pathname = url.pathname.replace(/\/$/, ''); let file = pathname.substring(1); diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index d5c66d920b15..4e3cfe1eeef3 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -22,18 +22,23 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * @param {string} path - * @param {number} max_age - * @param {boolean} immutable + * @param {boolean} client */ -function serve(path, max_age, immutable = false) { +function serve(path, client = false) { return ( fs.existsSync(path) && sirv(path, { etag: true, - maxAge: max_age, - immutable, gzip: true, - brotli: true + brotli: true, + setHeaders: + client && + ((res, pathname) => { + // only apply to build directory, not e.g. version.json + if (pathname.startsWith(`/${manifest.appDir}/immutable/`)) { + res.setHeader('cache-control', 'public,max-age=31536000,immutable'); + } + }) }) ); } @@ -126,9 +131,9 @@ function get_origin(headers) { export const handler = sequence( [ - serve(path.join(__dirname, '/client'), 31536000, true), - serve(path.join(__dirname, '/static'), 0), - serve(path.join(__dirname, '/prerendered'), 0), + serve(path.join(__dirname, '/client'), true), + serve(path.join(__dirname, '/static')), + serve(path.join(__dirname, '/prerendered')), ssr ].filter(Boolean) ); diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index af46b97c8e5a..23e0070736c7 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -181,7 +181,7 @@ async function v1(builder, external) { ...prerendered_pages, ...prerendered_redirects, { - src: `/${builder.config.kit.appDir}/.+`, + src: `/${builder.config.kit.appDir}/immutable/.+`, headers: { 'cache-control': 'public, immutable, max-age=31536000' } diff --git a/packages/kit/src/core/build/build_client.js b/packages/kit/src/core/build/build_client.js index 1e7f23bc8cd5..8bea2daf31b1 100644 --- a/packages/kit/src/core/build/build_client.js +++ b/packages/kit/src/core/build/build_client.js @@ -59,7 +59,7 @@ export async function build_client({ build: { cssCodeSplit: true, manifest: true, - outDir: client_out_dir, + outDir: `${client_out_dir}/immutable`, polyfillDynamicImport: false, rollupOptions: { input, @@ -95,7 +95,9 @@ export async function build_client({ const { chunks, assets } = await create_build(merged_config); /** @type {import('vite').Manifest} */ - const vite_manifest = JSON.parse(fs.readFileSync(`${client_out_dir}/manifest.json`, 'utf-8')); + const vite_manifest = JSON.parse( + fs.readFileSync(`${client_out_dir}/immutable/manifest.json`, 'utf-8') + ); const entry = posixify(client_entry_file); const entry_js = new Set(); diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 289abecf0cf2..8cf13eb49bb0 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -69,7 +69,7 @@ export class Server { manifest, method_override: ${s(config.kit.methodOverride)}, paths: { base, assets }, - prefix: assets + '/${config.kit.appDir}/', + prefix: assets + '/${config.kit.appDir}/immutable/', prerender: { default: ${config.kit.prerender.default}, enabled: ${config.kit.prerender.enabled} diff --git a/packages/kit/src/core/build/build_service_worker.js b/packages/kit/src/core/build/build_service_worker.js index e193242d665d..6b43554bdd2d 100644 --- a/packages/kit/src/core/build/build_service_worker.js +++ b/packages/kit/src/core/build/build_service_worker.js @@ -44,7 +44,7 @@ export async function build_service_worker( export const build = [ ${Array.from(build) - .map((file) => `${s(`${config.kit.paths.base}/${config.kit.appDir}/${file}`)}`) + .map((file) => `${s(`${config.kit.paths.base}/${config.kit.appDir}/immutable/${file}`)}`) .join(',\n\t\t\t\t')} ]; diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js index 61def523cc7f..4bd182b64753 100644 --- a/packages/kit/src/core/build/index.js +++ b/packages/kit/src/core/build/index.js @@ -26,15 +26,18 @@ export async function build(config, { log }) { const { manifest_data } = sync.all(config); + // TODO this is so that Vite's preloading works. Unfortunately, it fails + // during `svelte-kit preview`, because we use a local asset path. If Vite + // used relative paths, I _think_ this could get fixed. Issue here: + // https://github.com/vitejs/vite/issues/2009 + const { base, assets } = config.kit.paths; + const assets_base = `${assets || base}/${config.kit.appDir}/immutable/`; + const options = { cwd, config, build_dir, - // TODO this is so that Vite's preloading works. Unfortunately, it fails - // during `svelte-kit preview`, because we use a local asset path. If Vite - // used relative paths, I _think_ this could get fixed. Issue here: - // https://github.com/vitejs/vite/issues/2009 - assets_base: `${config.kit.paths.assets || config.kit.paths.base}/${config.kit.appDir}/`, + assets_base, manifest_data, output_dir, client_entry_file: path.relative(cwd, `${get_runtime_path(config)}/client/start.js`), @@ -65,8 +68,8 @@ export async function build(config, { log }) { const files = new Set([ ...static_files, - ...client.chunks.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`), - ...client.assets.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`) + ...client.chunks.map((chunk) => `${config.kit.appDir}/immutable/${chunk.fileName}`), + ...client.assets.map((chunk) => `${config.kit.appDir}/immutable/${chunk.fileName}`) ]); // TODO is this right? diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index 5cf910db557a..90adb40174d8 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -67,8 +67,12 @@ export async function preview({ port, host, https: use_https = false }) { scoped( assets, sirv(join(config.kit.outDir, 'output/client'), { - maxAge: 31536000, - immutable: true + setHeaders: (res, pathname) => { + // only apply to build directory, not e.g. version.json + if (pathname.startsWith(`/${config.kit.appDir}/immutable`)) { + res.setHeader('cache-control', 'public,max-age=31536000,immutable'); + } + } }) ), diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 8bec7f67304e..a8af8739de2e 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -347,7 +347,7 @@ test.describe.parallel('Imports', () => { ]); } else { expect(sources[0].startsWith('data:image/png;base64,')).toBeTruthy(); - expect(sources[1]).toBe(`${baseURL}/_app/assets/large-3183867c.jpg`); + expect(sources[1]).toBe(`${baseURL}/_app/immutable/assets/large-3183867c.jpg`); } }); }); @@ -2649,3 +2649,13 @@ test.describe.parallel('XSS', () => { ); }); }); + +test.describe.parallel('Version', () => { + test('does not serve version.json with an immutable cache header', async ({ request }) => { + // this isn't actually a great test, because caching behaviour is down to adapters. + // but it's better than nothing + const response = await request.get('/_app/version.json'); + const headers = response.headers(); + expect(headers['cache-control'] || '').not.toContain('immutable'); + }); +}); diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js index 0d8ba54090df..22b9e99d14a0 100644 --- a/packages/kit/test/apps/options-2/test/test.js +++ b/packages/kit/test/apps/options-2/test/test.js @@ -21,7 +21,7 @@ test.describe.parallel('Service worker', () => { const response = await request.get('/basepath/service-worker.js'); const content = await response.text(); - expect(content).toMatch(/\/_app\/start-[a-z0-9]+\.js/); + expect(content).toMatch(/\/_app\/immutable\/start-[a-z0-9]+\.js/); }); test('does not register /basepath/service-worker.js', async ({ page }) => {