Skip to content
Open
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
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default [
'packages/kit/test/apps/**/*',
'packages/kit/test/build-errors/**/*',
'packages/kit/test/prerendering/**/*',
'packages/kit/test/version-chunk-rotation/**/*',
'packages/test-redirect-importer/index.js',
'packages/adapter-netlify/test/preview.js'
]
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@
"browser": "./src/runtime/app/env/public/client.js",
"default": "./src/runtime/app/env/public/server.js"
},
"#app/env/version": {
"browser": "./src/runtime/app/env/version/client.js",
"default": "./src/runtime/app/env/version/server.js"
},
"#internal": {
"browser": "./src/exports/internal/client.js",
"default": "./src/exports/internal/server/index.js"
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import path from 'node:path';
import { styleText } from 'node:util';
import { hash } from '../../utils/hash.js';
import { resolve_entry } from '../../utils/filesystem.js';
import { posixify } from '../../utils/os.js';
import { s } from '../../utils/misc.js';
import { load_error_page, load_template } from '../config/index.js';
import { payload_hash } from '../utils.js';
import { write_if_changed } from './utils.js';
import { escape_html } from '../../utils/escape.js';

Expand Down Expand Up @@ -56,7 +56,7 @@ export const options = {
)},
error
},
version_hash: ${s(hash(config.kit.version.name))}
version_hash: ${s(payload_hash(config.kit))}
};

export async function get_hooks() {
Expand Down
40 changes: 40 additions & 0 deletions packages/kit/src/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,50 @@ import { fileURLToPath } from 'node:url';
import { styleText } from 'node:util';
import { to_fs } from '../utils/vite.js';
import { noop } from '../utils/functions.js';
import { hash } from '../utils/hash.js';
import { posixify } from '../utils/os.js';

/**
<<<<<<< version-chunk-rotation-fix

@vercel vercel Bot Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unresolved git merge conflict markers were committed in packages/kit/src/core/utils.js, producing invalid JavaScript that breaks parsing/build of the entire kit package.

Fix on Vercel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. There's a suggested fix on this thread that removes the three conflict markers while keeping the new payload_hash function and using the version-3 wording for the runtime_directory JSDoc — applying it resolves the conflict cleanly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. There's a committed fix suggestion on this comment (merge-conflict-markers-utils) that removes all three conflict markers — it keeps the new payload_hash function and uses the version-3 wording (Resolved path of the runtime directory posix-ified) for the runtime_directory JSDoc. Applying it resolves the conflict and restores valid JS.

* Returns a deterministic identifier for the `globalThis.__sveltekit_${payload_hash}`
* payload global.
*
* It must not be derived from `kit.version.name`: the name is inlined into
* content-hashed client chunks in `__SVELTEKIT_PAYLOAD__` via the
* `$env/dynamic/public` virtual module, so a version-derived identifier rotates the
* hashed filename of every chunk referencing it on each deploy (#12260).
*
* It must still distinguish SvelteKit apps from different projects embedded in the
* same document (#9576), so it is derived from the project's `package.json` name
* plus `paths.base` and `appDir` — all deterministic across repeated config loads
* within a build and across machines.
*
* @param {import('types').ValidatedKitConfig} kit
* @returns {string}
*/
export function payload_hash(kit) {
if (kit.embedded) return hash(kit.version.name);

const root = path.dirname(kit.outDir);

let name = '';

try {
name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8')).name ?? '';
} catch {
// TODO log error or similiar
}

if (!name) name = path.basename(root);

return hash(`${name}\n${kit.paths.base}\n${kit.appDir}`);
}

/**
* Resolved path of the `runtime` directory
=======
* Resolved path of the `runtime` directory posix-ified
>>>>>>> version-3
*
* TODO Windows issue:
* Vite or sth else somehow sets the driver letter inconsistently to lower or upper case depending on the run environment.
Expand Down
23 changes: 21 additions & 2 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import * as sync from '../../core/sync/sync.js';
import { create_assets } from '../../core/sync/create_manifest_data/index.js';
import { load_and_validate_params } from '../../utils/params.js';
import { runtime_directory, logger } from '../../core/utils.js';
import { runtime_directory, logger, payload_hash } from '../../core/utils.js';
import { generate_manifest } from '../../core/generate_manifest/index.js';
import { build_server_nodes } from './build/build_server.js';
import { find_deps, resolve_symlinks } from './build/utils.js';
Expand Down Expand Up @@ -217,6 +217,12 @@ function resolve_root(vite_config) {
return posixify(vite_config.root ? path.resolve(vite_config.root) : process.cwd());
}

/**
* config loads twice so we cache the value of the first load so client and server bundles read the same value
* @type {string | undefined}
*/
let resolved_version_name;

/**
* @return {Plugin}
*/
Expand Down Expand Up @@ -341,7 +347,20 @@ function kit({ svelte_config }) {
out_dir = posixify(kit.outDir);
out = `${out_dir}/output`;

version_hash = hash(kit.version.name);
if (is_build) {
if (resolved_version_name === undefined) {
resolved_version_name = kit.version.name;
} else if (kit.version.name !== resolved_version_name) {
console.warn(
`svelte.config.js computed a different kit.version.name on each load ` +
`("${resolved_version_name}" vs "${kit.version.name}"). Using the first value ` +
`for the whole build — make version.name deterministic to silence this warning.`
);
}
kit.version.name = resolved_version_name;
}

version_hash = payload_hash(kit);

kit_global = is_build
? `globalThis.__sveltekit_${version_hash}`
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/app/env/internal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const version = __SVELTEKIT_APP_VERSION__;
export { version } from '#app/env/version';
export let building = false;
export let prerendering = false;

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/app/env/version/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const version = /** @type {string} */ (__SVELTEKIT_PAYLOAD__?.version);
1 change: 1 addition & 0 deletions packages/kit/src/runtime/app/env/version/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const version = __SVELTEKIT_APP_VERSION__;
5 changes: 4 additions & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,10 @@ export async function render_response({

const blocks = [];

const properties = [`base: ${base_expression}`];
const properties = [
`base: ${base_expression}`,
`version: ${devalue.uneval(__SVELTEKIT_APP_VERSION__)}`
];

if (paths.assets) {
properties.push(`assets: ${s(paths.assets)}`);
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/types/global-private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ declare global {
const __SVELTEKIT_PAYLOAD__: {
/** The basepath, usually relative to the current page */
base: string;
/** determinsitically computed from package name + config.appDir + config.base */
version?: string;
/** Path to externally-hosted assets */
assets?: string;
/** Public environment variables */
Expand Down
22 changes: 22 additions & 0 deletions packages/kit/test/version-chunk-rotation/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "test-version-chunk-rotation-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "svelte-kit sync && vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync",
"check": "svelte-kit sync && tsc && svelte-check"
},
"devDependencies": {
"@sveltejs/adapter-auto": "workspace:^",
"@sveltejs/kit": "workspace:^",
"@sveltejs/vite-plugin-svelte": "catalog:",
"svelte": "catalog:",
"svelte-check": "catalog:",
"typescript": "catalog:",
"vite": "catalog:"
}
}
5 changes: 5 additions & 0 deletions packages/kit/test/version-chunk-rotation/app/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare global {
namespace App {}
}

export {};
11 changes: 11 additions & 0 deletions packages/kit/test/version-chunk-rotation/app/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const sharedMessage = 'shared between routes';

/** @param {number} n */
export function double(n) {
return n * 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import { double, sharedMessage } from '$lib/shared.js';
</script>

<h1>home</h1>
<p>{sharedMessage}: {double(21)}</p>
<a href="/about">about</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import { double, sharedMessage } from '$lib/shared.js';
</script>

<h1>about</h1>
<p>{sharedMessage}: {double(50)}</p>
<a href="/">home</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const prerender = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
import { sharedMessage } from '$lib/shared.js';
</script>

<h1>prerendered</h1>
<p>{sharedMessage}</p>
8 changes: 8 additions & 0 deletions packages/kit/test/version-chunk-rotation/app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true
},
"extends": "./.svelte-kit/tsconfig.json"
}
32 changes: 32 additions & 0 deletions packages/kit/test/version-chunk-rotation/app/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as path from 'node:path';
import process from 'node:process';
import { sveltekit } from '@sveltejs/kit/vite';
import adapter from '../../../../adapter-auto/index.js';

/** @type {import('vite').UserConfig} */
const config = {
build: {
minify: false
},

clearScreen: false,

logLevel: 'silent',

plugins: [
sveltekit({
adapter: adapter(),
version: {
name: process.env.SK_VERSION || Date.now().toString()
}
})
],

server: {
fs: {
allow: [path.resolve('../../../src')]
}
}
};

export default config;
88 changes: 88 additions & 0 deletions packages/kit/test/version-chunk-rotation/classify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';

/**
* @typedef {object} Chunk
* @property {string} bytes_hash
* @property {boolean} contains_version
*/

const VERSION_DERIVED_PAYLOAD_KEY = /__sveltekit_(?!dev\b)[A-Za-z0-9]+/;

/** @param {string} code */
const bytes_hash = (code) => createHash('sha256').update(code).digest('hex').slice(0, 12);

/**
* @param {string} dir
* @param {string} [base]
* @param {string[]} [found]
* @returns {string[]}
*/
function js_files(dir, base = dir, found = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) js_files(abs, base, found);
else if (entry.name.endsWith('.js')) found.push(path.relative(base, abs));
}
return found.sort();
}

/**
* @param {string} dir
* @param {string} version
* @returns {Map<string, Chunk>}
*/
export function snapshot(dir, version) {
/** @type {Map<string, Chunk>} */
const chunks = new Map();

for (const filename of js_files(dir)) {
const code = fs.readFileSync(path.join(dir, filename), 'utf-8');

chunks.set(filename, {
bytes_hash: bytes_hash(code),
contains_version: code.includes(version) || VERSION_DERIVED_PAYLOAD_KEY.test(code)
});
}

return chunks;
}

/**
* @param {Map<string, Chunk>} before
* @param {Map<string, Chunk>} after
*/
export function compare(before, after) {
/** @type {Array<{ name: string, contains_version: boolean }>} */
const rotated = [];
/** @type {Array<{ name: string, contains_version: boolean }>} */
const mutable = [];

for (const [name, chunk] of before) {
const replacement = after.get(name);
if (!replacement) {
rotated.push({ name, contains_version: chunk.contains_version });
} else if (replacement.bytes_hash !== chunk.bytes_hash) {
mutable.push({
name,
contains_version: chunk.contains_version || replacement.contains_version
});
}
}

return { total: before.size, rotated, mutable };
}

/** @param {ReturnType<typeof compare>} result */
export function format_report(result) {
/** @param {{ name: string, contains_version: boolean }} finding */
const line = ({ name, contains_version }) =>
`${name}${contains_version ? ' ← contains the version' : ''}`;

return [
`${result.total} chunks total · ${result.rotated.length} rotated · ${result.mutable.length} mutable (same name, new bytes)`,
...result.mutable.map((finding) => ` MUTABLE ${line(finding)}`),
...result.rotated.map((finding) => ` ROTATED ${line(finding)}`)
].join('\n');
}
Loading
Loading