Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/stable-version-build-hashes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: preserve shared client chunk hashes when the app version changes
1 change: 1 addition & 0 deletions packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export async function load_explicit_env(kit, file, root, mode) {
logLevel: 'silent',
mode,
define: {
__SVELTEKIT_PAYLOAD__: 'undefined', // coming in through static import in env/internal.js but will end up unused
__SVELTEKIT_APP_VERSION__: JSON.stringify(kit.version.name) // needed by $app/env
},
resolve: {
Expand Down
5 changes: 0 additions & 5 deletions packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,6 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
write_if_changed(
`${output}/app.js`,
dedent`
// in dev, this makes Vite inject its client as this module's first dependency,
// so that global constant replacements are installed before any other module
// (including user hooks) evaluates. In build it's inert.
import.meta.hot;

${
client_hooks_file
? `import * as client_hooks from '${relative_path(output, client_hooks_file)}';`
Expand Down
29 changes: 22 additions & 7 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ function kit({ svelte_config }) {
new_config.define = {
...define,
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0',
__SVELTEKIT_PAYLOAD__: kit_global,
__SVELTEKIT_PAYLOAD__: kit_global, // only relevant when bundleStrategy !== 'split'
__SVELTEKIT_HAS_SERVER_LOAD__: 'true',
__SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true'
};
Expand Down Expand Up @@ -625,7 +625,7 @@ function kit({ svelte_config }) {
return create_sveltekit_env_public(
explicit_env_config,
env,
`const env = ${kit_global}.env;`
`import { payload } from ${s(`${runtime_directory}/client/payload.js`)};\nconst env = payload.env;`
);

case sveltekit_env_public_server:
Expand Down Expand Up @@ -1301,6 +1301,7 @@ function kit({ svelte_config }) {
client_input['bundle'] = `${runtime_directory}/client/bundle.js`;
} else {
client_input['entry/start'] = `${runtime_directory}/client/entry.js`;
client_input['entry/payload'] = `${runtime_directory}/client/payload.js`;
client_input['entry/app'] = `${out_dir}/generated/client-optimized/app.js`;
manifest_data.nodes.forEach((node, i) => {
if (node.component || node.universal) {
Expand Down Expand Up @@ -1409,7 +1410,8 @@ function kit({ svelte_config }) {
}
},
define: {
__SVELTEKIT_PAYLOAD__: kit_global
__SVELTEKIT_PAYLOAD__:
svelte_config.kit.output.bundleStrategy !== 'split' ? kit_global : 'undefined'
}
}
},
Expand Down Expand Up @@ -1672,15 +1674,28 @@ function kit({ svelte_config }) {
);

if (svelte_config.kit.output.bundleStrategy === 'split') {
const start = deps_of(`${runtime_directory}/client/entry.js`);
const start_entry = posixify(path.relative(root, `${runtime_directory}/client/entry.js`));
const start = find_deps(vite_manifest, start_entry, false, root);
const runtime_entry = resolve_symlinks(vite_manifest, start_entry, root).chunk
.dynamicImports?.[0]; // client/entry.js dynamically imports client/client-entry.js
if (!runtime_entry) throw new Error('Could not find the client runtime chunk');
const runtime = find_deps(vite_manifest, runtime_entry, false, root);
const app = deps_of(`${out_dir}/generated/client-optimized/app.js`);

build_data.client = {
start: start.file,
app: app.file,
imports: [...start.imports, ...app.imports],
stylesheets: [...start.stylesheets, ...app.stylesheets],
fonts: [...start.fonts, ...app.fonts],
imports: Array.from(
new Set([
...start.imports,
runtime.file,
...runtime.imports,
app.file,
...app.imports
])
),
stylesheets: [...start.stylesheets, ...runtime.stylesheets, ...app.stylesheets],
fonts: [...start.fonts, ...runtime.fonts, ...app.fonts],
uses_env_dynamic_public
};

Expand Down
8 changes: 4 additions & 4 deletions packages/kit/src/runtime/app/env/internal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const version = __SVELTEKIT_APP_VERSION__;
import { BROWSER } from 'esm-env';
import { payload } from '../../client/payload.js';

export const version = BROWSER ? payload.version : __SVELTEKIT_APP_VERSION__;
export let building = false;
export let prerendering = false;

Expand All @@ -9,6 +12,3 @@ export function set_building() {
export function set_prerendering() {
prerendering = true;
}

// force /@vite/client to be injected
import.meta.hot;
6 changes: 4 additions & 2 deletions packages/kit/src/runtime/app/paths/internal/client.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const base = __SVELTEKIT_PAYLOAD__?.base ?? __SVELTEKIT_PATHS_BASE__;
export const assets = __SVELTEKIT_PAYLOAD__?.assets ?? base ?? __SVELTEKIT_PATHS_ASSETS__;
import { payload } from '../../../client/payload.js';

export const base = payload.base ?? __SVELTEKIT_PATHS_BASE__;
export const assets = payload.assets ?? base ?? __SVELTEKIT_PATHS_ASSETS__;
export const app_dir = __SVELTEKIT_APP_DIR__;
export const hash_routing = __SVELTEKIT_HASH_ROUTING__;
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/client/bundle.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* if `bundleStrategy` is 'single' or 'inline', this file is used as the entry point */

import * as kit from './entry.js';
import * as kit from './client-entry.js';

// @ts-expect-error
import * as app from '__sveltekit/manifest';
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/runtime/client/client-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// we expose this as a separate entry point (rather than treating client.js as the entry point)
// so that everything other than `start`/`load_css` can be treeshaken
export { start, load_css } from './client.js';
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
} from '../shared.js';
import { get_message, get_status } from '../../utils/error.js';
import { page, update, navigating, updated } from './state.svelte.js';
import { payload } from './payload.js';
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
import { noop_span } from '../telemetry/noop.js';
import { read_ndjson } from './ndjson.js';
Expand Down Expand Up @@ -343,8 +344,8 @@ export async function start(_app, _target, hydrate) {
);
}

if (__SVELTEKIT_PAYLOAD__.data) {
const { q = {}, p = {}, l = {}, f = {} } = __SVELTEKIT_PAYLOAD__.data;
if (payload.data) {
const { q = {}, p = {}, l = {}, f = {} } = payload.data;

// store the whole nodes — error records seed the corresponding
// resources in a failed state when they are created during hydration
Expand Down
27 changes: 24 additions & 3 deletions packages/kit/src/runtime/client/entry.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
// we expose this as a separate entry point (rather than treating client.js as the entry point)
// so that everything other than `start`/`load_css` can be treeshaken
export { start, load_css } from './client.js';
/* in development or if `bundleStrategy` is 'split', this file is used as the entry point */

import { set_payload } from './payload.js';

/** @type {Promise<typeof import('./client-entry.js')>} */
let client;

/** @param {import('types').SvelteKitPayload} payload */
export function init(payload) {
set_payload(payload);
// Importing the client after setting the payload ensures that modules such as
// `$app/paths` can read it during initialization.
client ??= import('./client-entry.js');
}

/** @param {Parameters<import('./client-entry.js').start>} args */
export async function start(...args) {
Comment thread
dummdidumm marked this conversation as resolved.
return (await client).start(...args);
}

/** @param {Parameters<import('./client-entry.js').load_css>} args */
export async function load_css(...args) {
return (await client).load_css(...args);
}
17 changes: 17 additions & 0 deletions packages/kit/src/runtime/client/payload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @import {SvelteKitPayload} from 'types'; */

/**
* Code inside the SvelteKit client runtime should only use this, not the global,
* so that the file hashes stay stable between rebuilds as long as the SvelteKit runtime doesn't change
*/
export let payload = __SVELTEKIT_PAYLOAD__ ?? /** @type {SvelteKitPayload} */ ({});

/** @param {SvelteKitPayload} value */
export function set_payload(value) {
payload = value;
}

// this makes Vite inject its dev client code as this module's first dependency
// so that global constant replacements are done before any other module evaluates.
// For build, it's inert.
import.meta.hot;
13 changes: 7 additions & 6 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ export async function render_response({

const blocks = [];

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

if (paths.assets) {
properties.push(`assets: ${s(paths.assets)}`);
Expand All @@ -427,7 +427,9 @@ export async function render_response({
if (client.inline) {
app_declaration = `const app = ${global}.app.app;`;
} else if (client.app) {
app_declaration = `const app = await import(${s(prefixed(client.app))});`;
app_declaration = `const kit = await import(${s(prefixed(client.start))});
kit.init(${global});
const app = await import(${s(prefixed(client.app))});`;
} else {
app_declaration = `const { app } = await import(${s(prefixed(client.start))});`;
}
Expand Down Expand Up @@ -521,10 +523,9 @@ export async function render_response({

${serialized_data}${global}.app.start(${args.join(', ')});`
: client.app
? `Promise.all([
import(${s(prefixed(client.start))}),
import(${s(prefixed(client.app))})
]).then(([kit, app]) => {
? `import(${s(prefixed(client.start))}).then(async (kit) => {
kit.init(${global});
const app = await import(${s(prefixed(client.app))});
${serialized_data}kit.start(app, ${args.join(', ')});
});`
: `import(${s(prefixed(client.start))}).then((app) => {
Expand Down
22 changes: 6 additions & 16 deletions packages/kit/src/types/global-private.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RemoteFunctionData } from 'types';
import { SvelteKitPayload } from 'types';

declare global {
const __SVELTEKIT_ADAPTER_NAME__: string;
Expand Down Expand Up @@ -33,21 +33,11 @@ declare global {
* Used for treeshaking universal load code from client bundles when no universal loads exist.
*/
const __SVELTEKIT_HAS_UNIVERSAL_LOAD__: boolean;
/** The `__sveltekit_abc123` object in the init `<script>` */
const __SVELTEKIT_PAYLOAD__: {
/** The basepath, usually relative to the current page */
base: string;
/** Path to externally-hosted assets */
assets?: string;
/** Public environment variables */
env?: Record<string, string>;
/** Serialized data from query/form/command functions */
data?: RemoteFunctionData;
/** Create a placeholder promise */
defer?: (id: number) => Promise<any>;
/** Resolve a placeholder promise */
resolve?: (data: { id: number; data: any; error: any }) => void;
};
/**
* The `__sveltekit_abc123` object in the init `<script>`.
* Should only be used when bundleStrategy !== 'inline' to avoid SvelteKit runtime changing on every build, preventing cacheability.
*/
const __SVELTEKIT_PAYLOAD__: SvelteKitPayload;
/**
* The Vite `root` setting used to construct paths to nodes and components
* for the SSR manifest during development
Expand Down
18 changes: 18 additions & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,5 +742,23 @@ export interface RequestStore {
state: RequestState;
}

/** Type of the `__sveltekit_abc123` object in the init `<script>` */
export interface SvelteKitPayload {
/** The application version */
version: string;
/** The basepath, usually relative to the current page */
base: string;
/** Path to externally-hosted assets */
assets?: string;
/** Public environment variables */
env?: Record<string, string>;
/** Serialized data from query/form/command functions */
data?: RemoteFunctionData;
/** Create a placeholder promise */
defer?: (id: number) => Promise<any>;
/** Resolve a placeholder promise */
resolve?: (data: { id: number; data: any; error: any }) => void;
}

export * from '../exports/index.js';
export * from './private.js';
5 changes: 3 additions & 2 deletions packages/kit/test/apps/async/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && tsc && svelte-check",
"test": "pnpm test:dev && pnpm test:build",
"test": "pnpm test:unit && pnpm test:dev && pnpm test:build",
"test:dev": "DEV=true playwright test",
"test:build": "playwright test"
"test:build": "playwright test",
"test:unit": "vitest run"
},
"devDependencies": {
"@sveltejs/kit": "workspace:^",
Expand Down
48 changes: 48 additions & 0 deletions packages/kit/test/apps/async/unit-test/node.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { execSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { expect, test } from 'vitest';

const timeout = 60_000;

test('SvelteKit runtime JS files stay stable between rebuilds', { timeout }, () => {
execSync('pnpm build', {
cwd: path.join(import.meta.dirname, '../'),
stdio: 'pipe',
timeout
});

const before = get_client_chunk_name();

execSync('pnpm build', {
cwd: path.join(import.meta.dirname, '../'),
stdio: 'pipe',
timeout
});

expect(get_client_chunk_name()).toBe(before);

function get_client_chunk_name() {
const start_file = fs
.readdirSync(
path.join(import.meta.dirname, '../.svelte-kit/output/client/_app/immutable/entry')
)
.find((file) => file.startsWith('start.'));
if (!start_file) {
throw new Error('start file not found, test needs adjustment');
}
const start_content = fs.readFileSync(
path.join(
import.meta.dirname,
'../.svelte-kit/output/client/_app/immutable/entry',
start_file
),
'utf-8'
);
const chunk_file = start_content.match(/import .+? from "\.\.\/chunks\/([^"]+)"/)?.[1];
if (!chunk_file) {
throw new Error('chunk file not found, test needs adjustment');
}
return chunk_file;
}
});
11 changes: 6 additions & 5 deletions packages/kit/test/apps/async/vite.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as path from 'node:path';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

/** @type {import('vite').UserConfig} */
const config = {
export default defineConfig({
build: {
minify: false
},
Expand All @@ -25,7 +25,8 @@ const config = {
fs: {
allow: [path.resolve('../../../src')]
}
},
test: {
include: ['unit-test/**/node.spec.js']
}
};

export default config;
});
Loading