diff --git a/.changeset/stable-version-build-hashes.md b/.changeset/stable-version-build-hashes.md new file mode 100644 index 000000000000..b961c466d5d9 --- /dev/null +++ b/.changeset/stable-version-build-hashes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve shared client chunk hashes when the app version changes diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 63c493e61875..de264df0979d 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -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: { diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index 42b3e940282d..4d465a2c604a 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -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)}';` diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index fec2df803737..492c1bf7325a 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -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' }; @@ -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: @@ -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) { @@ -1409,7 +1410,8 @@ function kit({ svelte_config }) { } }, define: { - __SVELTEKIT_PAYLOAD__: kit_global + __SVELTEKIT_PAYLOAD__: + svelte_config.kit.output.bundleStrategy !== 'split' ? kit_global : 'undefined' } } }, @@ -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 }; diff --git a/packages/kit/src/runtime/app/env/internal.js b/packages/kit/src/runtime/app/env/internal.js index 1d438b38e870..ec8fa432a666 100644 --- a/packages/kit/src/runtime/app/env/internal.js +++ b/packages/kit/src/runtime/app/env/internal.js @@ -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; @@ -9,6 +12,3 @@ export function set_building() { export function set_prerendering() { prerendering = true; } - -// force /@vite/client to be injected -import.meta.hot; diff --git a/packages/kit/src/runtime/app/paths/internal/client.js b/packages/kit/src/runtime/app/paths/internal/client.js index a5679afff6ef..a35036645816 100644 --- a/packages/kit/src/runtime/app/paths/internal/client.js +++ b/packages/kit/src/runtime/app/paths/internal/client.js @@ -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__; diff --git a/packages/kit/src/runtime/client/bundle.js b/packages/kit/src/runtime/client/bundle.js index f375d02e8355..6833766ccc8e 100644 --- a/packages/kit/src/runtime/client/bundle.js +++ b/packages/kit/src/runtime/client/bundle.js @@ -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'; diff --git a/packages/kit/src/runtime/client/client-entry.js b/packages/kit/src/runtime/client/client-entry.js new file mode 100644 index 000000000000..74b0fb283a58 --- /dev/null +++ b/packages/kit/src/runtime/client/client-entry.js @@ -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'; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index cd55a9c83207..a0fc5d220ca5 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -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'; @@ -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 diff --git a/packages/kit/src/runtime/client/entry.js b/packages/kit/src/runtime/client/entry.js index 74b0fb283a58..82c3140d661f 100644 --- a/packages/kit/src/runtime/client/entry.js +++ b/packages/kit/src/runtime/client/entry.js @@ -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} */ +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} args */ +export async function start(...args) { + return (await client).start(...args); +} + +/** @param {Parameters} args */ +export async function load_css(...args) { + return (await client).load_css(...args); +} diff --git a/packages/kit/src/runtime/client/payload.js b/packages/kit/src/runtime/client/payload.js new file mode 100644 index 000000000000..6b1a3952da67 --- /dev/null +++ b/packages/kit/src/runtime/client/payload.js @@ -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; diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index d945f01b7489..ef411446d0c0 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -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)}`); @@ -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))});`; } @@ -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) => { diff --git a/packages/kit/src/types/global-private.d.ts b/packages/kit/src/types/global-private.d.ts index 7c7bed787169..af2cf56e6957 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -1,4 +1,4 @@ -import { RemoteFunctionData } from 'types'; +import { SvelteKitPayload } from 'types'; declare global { const __SVELTEKIT_ADAPTER_NAME__: string; @@ -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 `