From 46eec8241094cfdffb57bb27e1b71368b6f311ff Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 18 Feb 2023 20:56:54 -0500 Subject: [PATCH] fix: separate framework code from app code (#8957) * separate app code from framework code * fix, simplify * changeset * DRY * try this? * Revert "try this?" This reverts commit a08b5d861d7088fc77b11607df3ca5f8af6ee4ae. * simplify * debug logs * Revert "debug logs" This reverts commit ec618f5a956853369338ba57e8cefc5fbd6d5655. * bump timeout * set public env in ` + +

The answer is {env.PUBLIC_ANSWER}

diff --git a/packages/adapter-static/test/test.js b/packages/adapter-static/test/test.js index 4069190d217f..0fe826aceefc 100644 --- a/packages/adapter-static/test/test.js +++ b/packages/adapter-static/test/test.js @@ -20,6 +20,11 @@ run('prerendered', (test) => { test('prerenders a referenced endpoint with implicit `prerender` setting', async ({ cwd }) => { assert.ok(fs.existsSync(`${cwd}/build/endpoint/implicit.json`)); }); + + test('exposes public env vars to the client', async ({ cwd, base, page }) => { + await page.goto(`${base}/public-env`); + assert.equal(await page.textContent('h1'), 'The answer is 42'); + }); }); run('spa', (test) => { diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 5b2c56bbb89d..a51c5948993b 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -3,11 +3,6 @@ import { runtime_base } from './utils.js'; /** * @typedef {'public' | 'private'} EnvType - * @typedef {{ - * public: Record; - * private: Record; - * prefix: string; - * }} EnvData */ /** @@ -44,12 +39,12 @@ export function create_dynamic_module(type, dev_values) { ); return `export const env = {\n${keys.join(',\n')}\n}`; } - return `export { ${type}_env as env } from '${runtime_base}/shared.js';`; + return `export { ${type}_env as env } from '${runtime_base}/shared-server.js';`; } /** * @param {EnvType} id - * @param {EnvData} env + * @param {import('types').Env} env * @returns {string} */ export function create_static_types(id, env) { @@ -63,15 +58,16 @@ export function create_static_types(id, env) { /** * @param {EnvType} id - * @param {EnvData} env + * @param {import('types').Env} env + * @param {string} prefix * @returns {string} */ -export function create_dynamic_types(id, env) { +export function create_dynamic_types(id, env, prefix) { const properties = Object.keys(env[id]) .filter((k) => valid_identifier.test(k)) .map((k) => `\t\t${k}: string;`); - const prefixed = `[key: \`${env.prefix}\${string}\`]`; + const prefixed = `[key: \`${prefix}\${string}\`]`; if (id === 'private') { properties.push(`\t\t${prefixed}: undefined;`); diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js index 80c3ba9ef2d1..2e59d145113d 100644 --- a/packages/kit/src/core/generate_manifest/index.js +++ b/packages/kit/src/core/generate_manifest/index.js @@ -86,7 +86,7 @@ export function generate_manifest({ build_data, relative_path, routes }) { assets: new Set(${s(assets)}), mimeTypes: ${s(get_mime_lookup(build_data.manifest_data))}, _: { - entry: ${s(build_data.client_entry)}, + client: ${s(build_data.client)}, nodes: [ ${(node_paths).map(loader).join(',\n\t\t\t\t')} ], diff --git a/packages/kit/src/core/sync/write_ambient.js b/packages/kit/src/core/sync/write_ambient.js index 6e028bcb056c..500ffe17c772 100644 --- a/packages/kit/src/core/sync/write_ambient.js +++ b/packages/kit/src/core/sync/write_ambient.js @@ -21,9 +21,10 @@ function read_description(filename) { } /** - * @param {import('../env.js').EnvData} env + * @param {import('types').Env} env + * @param {string} prefix */ -const template = (env) => ` +const template = (env, prefix) => ` ${GENERATED_COMMENT} /// @@ -35,10 +36,10 @@ ${read_description('$env+static+public.md')} ${create_static_types('public', env)} ${read_description('$env+dynamic+private.md')} -${create_dynamic_types('private', env)} +${create_dynamic_types('private', env, prefix)} ${read_description('$env+dynamic+public.md')} -${create_dynamic_types('public', env)} +${create_dynamic_types('public', env, prefix)} `; /** @@ -53,6 +54,6 @@ export function write_ambient(config, mode) { write_if_changed( path.join(config.outDir, 'ambient.d.ts'), - template({ ...env, prefix: config.env.publicPrefix }) + template(env, config.env.publicPrefix) ); } diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index 9137b9f868a8..36158be6ad25 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -106,9 +106,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { const hooks_file = resolve_entry(kit.files.hooks.client); - // String representation of __CLIENT__/manifest.js write_if_changed( - `${output}/manifest.js`, + `${output}/app.js`, trim(` ${hooks_file ? `import * as client_hooks from '${relative_path(output, hooks_file)}';` : ''} @@ -125,6 +124,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { hooks_file ? 'client_hooks.handleError || ' : '' }(({ error }) => { console.error(error) }), }; + + export { default as root } from '../root.svelte'; `) ); diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index c4e81d262ab2..0ccd98c42bd5 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { hash } from '../../runtime/hash.js'; import { posixify, resolve_entry } from '../../utils/filesystem.js'; import { s } from '../../utils/misc.js'; import { load_error_page, load_template } from '../config/index.js'; @@ -25,11 +26,11 @@ const server_template = ({ error_page }) => ` import root from '../root.svelte'; -import { set_assets, set_building, set_private_env, set_public_env, set_version } from '${runtime_directory}/shared.js'; - -set_version(${s(config.kit.version.name)}); +import { set_building } from '__sveltekit/environment'; +import { set_assets, set_private_env, set_public_env } from '${runtime_directory}/shared-server.js'; export const options = { + app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')}, csp: ${s(config.kit.csp)}, csrf_check_origin: ${s(config.kit.csrf.checkOrigin)}, embedded: ${config.kit.embedded}, @@ -50,7 +51,8 @@ export const options = { error: ({ status, message }) => ${s(error_page) .replace(/%sveltekit\.status%/g, '" + status + "') .replace(/%sveltekit\.error\.message%/g, '" + message + "')} - } + }, + version_hash: ${s(hash(config.kit.version.name))} }; export function get_hooks() { diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index a9791bfc334b..3c1499316212 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -6,6 +6,7 @@ import path from 'node:path'; * @param {import('vite').Manifest} manifest * @param {string} entry * @param {boolean} add_dynamic_css + * @returns {import('types').AssetDependencies} */ export function find_deps(manifest, entry, add_dynamic_css) { /** @type {Set} */ diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 5d7ca57ff606..05adc2776fb1 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -105,11 +105,19 @@ export async function dev(vite, vite_config, svelte_config) { assets: new Set(manifest_data.assets.map((asset) => asset.file)), mimeTypes: get_mime_lookup(manifest_data), _: { - entry: { - file: `${runtime_base}/client/start.js`, - imports: [], - stylesheets: [], - fonts: [] + client: { + start: { + file: `${runtime_base}/client/start.js`, + imports: [], + stylesheets: [], + fonts: [] + }, + app: { + file: `${svelte_config.kit.outDir}/generated/client/app.js`, + imports: [], + stylesheets: [], + fonts: [] + } }, nodes: manifest_data.nodes.map((node, index) => { return async () => { @@ -443,15 +451,13 @@ export async function dev(vite, vite_config, svelte_config) { await vite.ssrLoadModule(`${runtime_base}/server/index.js`) ); - const { set_assets, set_version, set_fix_stack_trace } = + const { set_assets, set_fix_stack_trace } = /** @type {import('types').ServerInternalModule} */ ( - await vite.ssrLoadModule(`${runtime_base}/shared.js`) + await vite.ssrLoadModule(`${runtime_base}/shared-server.js`) ); set_assets(assets); - set_version(svelte_config.kit.version.name); - set_fix_stack_trace(fix_stack_trace); const server = new Server(manifest); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 75f70711c277..5517deb6ea44 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -23,6 +23,7 @@ import { write_client_manifest } from '../../core/sync/write_client_manifest.js' import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; +import { hash } from '../../runtime/hash.js'; export { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; @@ -164,6 +165,8 @@ function kit({ svelte_config }) { const { kit } = svelte_config; const out = `${kit.outDir}/output`; + const version_hash = hash(kit.version.name); + /** @type {import('vite').ResolvedConfig} */ let vite_config; @@ -220,12 +223,7 @@ function kit({ svelte_config }) { const new_config = { resolve: { alias: [ - { - find: '__CLIENT__', - replacement: `${generated}/${is_build ? 'client-optimized' : 'client'}` - }, { find: '__SERVER__', replacement: `${generated}/server` }, - { find: '__GENERATED__', replacement: generated }, { find: '$app', replacement: `${runtime_directory}/app` }, ...get_config_aliases(kit) ] @@ -324,12 +322,15 @@ function kit({ svelte_config }) { async resolveId(id) { // treat $env/static/[public|private] as virtual - if (id.startsWith('$env/') || id === '__sveltekit/paths' || id === '$service-worker') { + if (id.startsWith('$env/') || id.startsWith('__sveltekit/') || id === '$service-worker') { return `\0${id}`; } }, async load(id, options) { + const browser = !options?.ssr; + const global = `__sveltekit_${version_hash}`; + if (options?.ssr === false && process.env.TEST !== 'true') { const normalized_cwd = vite.normalizePath(cwd); const normalized_lib = vite.normalizePath(kit.files.lib); @@ -356,16 +357,28 @@ function kit({ svelte_config }) { vite_config_env.command === 'serve' ? env.private : undefined ); case '\0$env/dynamic/public': + // populate `$env/dynamic/public` from `window` + if (browser) { + return `export const env = ${global}.env;`; + } + return create_dynamic_module( 'public', vite_config_env.command === 'serve' ? env.public : undefined ); case '\0$service-worker': return create_service_worker_module(svelte_config); + // for internal use only. it's published as $app/paths externally // we use this alias so that we won't collide with user aliases case '\0__sveltekit/paths': const { assets, base } = svelte_config.kit.paths; + + if (browser) { + return `export const base = ${s(base)}; +export const assets = ${global}.assets;`; + } + return `export const base = ${s(base)}; export let assets = ${assets ? s(assets) : 'base'}; @@ -373,6 +386,15 @@ export let assets = ${assets ? s(assets) : 'base'}; export function set_assets(path) { assets = path; }`; + + case '\0__sveltekit/environment': + const { version } = svelte_config.kit; + return `export const version = ${s(version.name)}; +export let building = false; + +export function set_building() { + building = true; +}`; } } }; @@ -460,30 +482,29 @@ export function set_assets(path) { input[name] = path.resolve(file); }); } else { - /** @type {Record} */ - input.start = `${runtime_directory}/client/start.js`; + input['entry/start'] = `${runtime_directory}/client/start.js`; + input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`; - manifest_data.nodes.forEach((node) => { - if (node.component) { - const resolved = path.resolve(node.component); - const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); + /** + * @param {string | undefined} file + */ + function add_input(file) { + if (!file) return; - const name = relative.startsWith('..') - ? path.basename(node.component) - : posixify(path.join('pages', relative)); - input[`components/${name}`] = resolved; - } + const resolved = path.resolve(file); + const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); - if (node.universal) { - const resolved = path.resolve(node.universal); - const relative = decodeURIComponent(path.relative(kit.files.routes, resolved)); + const name = relative.startsWith('..') + ? path.basename(file).replace(/^\+/, '') + : relative.replace(/(\\|\/)\+/g, '-').replace(/[\\/]/g, '-'); - const name = relative.startsWith('..') - ? path.basename(node.universal) - : posixify(path.join('pages', relative)); - input[`modules/${name}`] = resolved; - } - }); + input[`entry/${name}`] = resolved; + } + + for (const node of manifest_data.nodes) { + add_input(node.component); + add_input(node.universal); + } } new_config = { @@ -495,9 +516,13 @@ export function set_assets(path) { input, output: { format: 'esm', - entryFileNames: ssr ? '[name].js' : `${prefix}/[name]-[hash].js`, - chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name]-[hash].js`, - assetFileNames: `${prefix}/assets/[name]-[hash][extname]`, + // we use .mjs for client-side modules, because this signals to Chrome (when it + // reads the ) that it should parse the file as a module + // rather than as a script, preventing a double parse. Ideally we'd just use + // modulepreload, but Safari prevents that + entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].mjs`, + chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].mjs`, + assetFileNames: `${prefix}/assets/[name].[hash][extname]`, hoistTransitiveImports: false }, preserveEntrySignatures: 'strict' @@ -601,7 +626,7 @@ export function set_assets(path) { app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, manifest_data, service_worker: !!service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? - client_entry: null, + client: null, server_manifest }; @@ -658,11 +683,18 @@ export function set_assets(path) { /** @type {import('vite').Manifest} */ const client_manifest = JSON.parse(read(`${out}/client/${vite_config.build.manifest}`)); - build_data.client_entry = find_deps( - client_manifest, - posixify(path.relative('.', `${runtime_directory}/client/start.js`)), - false - ); + build_data.client = { + start: find_deps( + client_manifest, + posixify(path.relative('.', `${runtime_directory}/client/start.js`)), + false + ), + app: find_deps( + client_manifest, + posixify(path.relative('.', `${kit.outDir}/generated/client-optimized/app.js`)), + false + ) + }; const css = output.filter( /** @type {(value: any) => value is import('rollup').OutputAsset} */ diff --git a/packages/kit/src/internal.d.ts b/packages/kit/src/internal.d.ts index b7b17ee325a8..cfe9bf548544 100644 --- a/packages/kit/src/internal.d.ts +++ b/packages/kit/src/internal.d.ts @@ -1,3 +1,10 @@ +/** Internal version of $app/environment */ +declare module '__sveltekit/environment' { + export const building: boolean; + export const version: string; + export function set_building(): void; +} + /** Internal version of $app/paths */ declare module '__sveltekit/paths' { export const base: `/${string}`; diff --git a/packages/kit/src/runtime/app/environment.js b/packages/kit/src/runtime/app/environment.js index b301646c7f14..0d11daf31b1b 100644 --- a/packages/kit/src/runtime/app/environment.js +++ b/packages/kit/src/runtime/app/environment.js @@ -10,4 +10,4 @@ export const browser = BROWSER; */ export const dev = DEV; -export { building, version } from '../shared.js'; +export { building, version } from '__sveltekit/environment'; diff --git a/packages/kit/src/runtime/client/ambient.d.ts b/packages/kit/src/runtime/client/ambient.d.ts deleted file mode 100644 index 5f98d9891506..000000000000 --- a/packages/kit/src/runtime/client/ambient.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -declare module '__CLIENT__/manifest.js' { - import { CSRPageNodeLoader, ClientHooks, ParamMatcher } from 'types'; - - /** - * A list of all the error/layout/page nodes used in the app - */ - export const nodes: CSRPageNodeLoader[]; - - /** - * A list of all layout node ids that have a server load function. - * Pages are not present because it's shorter to encode it on the leaf itself. - */ - export const server_loads: number[]; - - /** - * A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which - * is parsed into an array of routes on startup. The numbers refer to the indices in `nodes`. - * If the leaf number is negative, it means it does use a server load function and the complement is the node index. - * The route layout and error nodes are not referenced, they are always number 0 and 1 and always apply. - */ - export const dictionary: Record; - - export const matchers: Record; - - export const hooks: ClientHooks; -} - -declare module '__GENERATED__/root.svelte' { - export { SvelteComponent as default } from 'svelte'; -} diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index eaba8054d1ff..fa382e677bcc 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -25,8 +25,6 @@ import { } from './fetcher.js'; import { parse } from './parse.js'; -import Root from '__GENERATED__/root.svelte'; -import { nodes, server_loads, dictionary, matchers, hooks } from '__CLIENT__/manifest.js'; import { base } from '__sveltekit/paths'; import { HttpError, Redirect } from '../control.js'; import { stores } from './singletons.js'; @@ -36,16 +34,6 @@ import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './const import { validate_common_exports } from '../../utils/exports.js'; import { compact } from '../../utils/array.js'; -const routes = parse(nodes, server_loads, dictionary, matchers); - -const default_layout_loader = nodes[0]; -const default_error_loader = nodes[1]; - -// we import the root layout/error nodes eagerly, so that -// connectivity errors after initialisation don't nuke the app -default_layout_loader(); -default_error_loader(); - // We track the scroll position associated with each history entry in sessionStorage, // rather than on history.state itself, because when navigation is driven by // popstate it's too late to update the scroll position associated with the @@ -65,11 +53,22 @@ function update_scroll_positions(index) { /** * @param {{ + * app: import('./types').SvelteKitApp; * target: HTMLElement; * }} opts * @returns {import('./types').Client} */ -export function create_client({ target }) { +export function create_client({ app, target }) { + const routes = parse(app); + + const default_layout_loader = app.nodes[0]; + const default_error_loader = app.nodes[1]; + + // we import the root layout/error nodes eagerly, so that + // connectivity errors after initialisation don't nuke the app + default_layout_loader(); + default_error_loader(); + const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement; /** @type {Array<((url: URL) => boolean)>} */ const invalidated = []; @@ -432,7 +431,7 @@ export function create_client({ target }) { page = /** @type {import('types').Page} */ (result.props.page); - root = new Root({ + root = new app.root({ target, props: { ...result.props, stores, components }, hydrate: true @@ -981,7 +980,7 @@ export function create_client({ target }) { /** @type {import('types').ServerDataNode | null} */ let server_data_node = null; - const default_layout_has_server_load = server_loads[0] === 0; + const default_layout_has_server_load = app.server_loads[0] === 0; if (default_layout_has_server_load) { // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use @@ -1311,6 +1310,21 @@ export function create_client({ target }) { after_navigate(); } + /** + * @param {unknown} error + * @param {import('types').NavigationEvent} event + * @returns {import('types').MaybePromise} + */ + function handle_error(error, event) { + if (error instanceof HttpError) { + return error.body; + } + return ( + app.hooks.handleError({ error, event }) ?? + /** @type {any} */ ({ message: event.route.id != null ? 'Internal Error' : 'Not Found' }) + ); + } + return { after_navigate: (fn) => { onMount(() => { @@ -1712,7 +1726,7 @@ export function create_client({ target }) { const server_data_node = server_data_nodes[i]; return load_node({ - loader: nodes[n], + loader: app.nodes[n], url, params, route, @@ -1799,21 +1813,6 @@ async function load_data(url, invalid) { return data; } -/** - * @param {unknown} error - * @param {import('types').NavigationEvent} event - * @returns {import('../../../types/private.js').MaybePromise} - */ -function handle_error(error, event) { - if (error instanceof HttpError) { - return error.body; - } - return ( - hooks.handleError({ error, event }) ?? - /** @type {any} */ ({ message: event.route.id != null ? 'Internal Error' : 'Not Found' }) - ); -} - function reset_focus() { const autofocus = document.querySelector('[autofocus]'); if (autofocus) { diff --git a/packages/kit/src/runtime/client/parse.js b/packages/kit/src/runtime/client/parse.js index 5a776d306246..d148b2888c72 100644 --- a/packages/kit/src/runtime/client/parse.js +++ b/packages/kit/src/runtime/client/parse.js @@ -1,13 +1,10 @@ import { exec, parse_route_id } from '../../utils/routing.js'; /** - * @param {import('types').CSRPageNodeLoader[]} nodes - * @param {number[]} server_loads - * @param {typeof import('__CLIENT__/manifest.js').dictionary} dictionary - * @param {Record boolean>} matchers + * @param {import('./types').SvelteKitApp} app * @returns {import('types').CSRRoute[]} */ -export function parse(nodes, server_loads, dictionary, matchers) { +export function parse({ nodes, server_loads, dictionary, matchers }) { const layouts_with_server_load = new Set(server_loads); return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => { diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js index aed3870f4e6d..0b28d108a9bc 100644 --- a/packages/kit/src/runtime/client/start.js +++ b/packages/kit/src/runtime/client/start.js @@ -1,21 +1,17 @@ import { DEV } from 'esm-env'; import { create_client } from './client.js'; import { init } from './singletons.js'; -import { set_assets, set_version, set_public_env } from '../shared.js'; /** - * @param {{ - * assets: string; - * env: Record; - * hydrate: Parameters[0]; - * target: HTMLElement; - * version: string; - * }} opts + * @param {import('./types').SvelteKitApp} app + * @param {string} hash + * @param {Parameters[0]} [hydrate] */ -export async function start({ assets, env, hydrate, target, version }) { - set_public_env(env); - set_assets(assets); - set_version(version); +export async function start(app, hash, hydrate) { + const target = /** @type {HTMLElement} */ ( + /** @type {HTMLScriptElement} */ (document.querySelector(`[data-sveltekit-hydrate="${hash}"]`)) + .parentNode + ); if (DEV && target === document.body) { console.warn( @@ -23,9 +19,7 @@ export async function start({ assets, env, hydrate, target, version }) { ); } - const client = create_client({ - target - }); + const client = create_client({ app, target }); init({ client }); diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index e72213822ac6..5439115f7c56 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -9,7 +9,43 @@ import { preloadData } from '$app/navigation'; import { SvelteComponent } from 'svelte'; -import { CSRPageNode, CSRPageNodeLoader, CSRRoute, Page, TrailingSlash, Uses } from 'types'; +import { + ClientHooks, + CSRPageNode, + CSRPageNodeLoader, + CSRRoute, + Page, + ParamMatcher, + TrailingSlash, + Uses +} from 'types'; + +export interface SvelteKitApp { + /** + * A list of all the error/layout/page nodes used in the app + */ + nodes: CSRPageNodeLoader[]; + + /** + * A list of all layout node ids that have a server load function. + * Pages are not present because it's shorter to encode it on the leaf itself. + */ + server_loads: number[]; + + /** + * A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which + * is parsed into an array of routes on startup. The numbers refer to the indices in `nodes`. + * If the leaf number is negative, it means it does use a server load function and the complement is the node index. + * The route layout and error nodes are not referenced, they are always number 0 and 1 and always apply. + */ + dictionary: Record; + + matchers: Record; + + hooks: ClientHooks; + + root: typeof SvelteComponent; +} export interface Client { // public API, exposed via $app/navigation diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 5874dd4ed771..1e9ba017a644 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -1,7 +1,7 @@ import { BROWSER, DEV } from 'esm-env'; import { writable } from 'svelte/store'; import { assets } from '__sveltekit/paths'; -import { version } from '../shared.js'; +import { version } from '__sveltekit/environment'; import { PRELOAD_PRIORITIES } from './constants.js'; /* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */ diff --git a/packages/kit/src/runtime/env/dynamic/private.js b/packages/kit/src/runtime/env/dynamic/private.js index d05b2fd8b10f..1965ee8bcbb0 100644 --- a/packages/kit/src/runtime/env/dynamic/private.js +++ b/packages/kit/src/runtime/env/dynamic/private.js @@ -1 +1 @@ -export { private_env as env } from '../../shared.js'; +export { private_env as env } from '../../shared-server.js'; diff --git a/packages/kit/src/runtime/env/dynamic/public.js b/packages/kit/src/runtime/env/dynamic/public.js index ba6c33230d07..919d034b5a1c 100644 --- a/packages/kit/src/runtime/env/dynamic/public.js +++ b/packages/kit/src/runtime/env/dynamic/public.js @@ -1 +1 @@ -export { public_env as env } from '../../shared.js'; +export { public_env as env } from '../../shared-server.js'; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 24d16cd3c077..670a3f7f3cc7 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,5 +1,5 @@ import { respond } from './respond.js'; -import { set_private_env, set_public_env } from '../shared.js'; +import { set_private_env, set_public_env } from '../shared-server.js'; import { options, get_hooks } from '__SERVER__/internal.js'; export class Server { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 1ac291329f5a..f866ea562876 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -8,7 +8,7 @@ import { s } from '../../../utils/misc.js'; import { Csp } from './csp.js'; import { uneval_action_response } from './actions.js'; import { clarify_devalue_error } from '../utils.js'; -import { version, public_env } from '../../shared.js'; +import { public_env } from '../../shared-server.js'; import { text } from '../../../exports/index.js'; // TODO rename this function/module @@ -57,11 +57,11 @@ export async function render_response({ } } - const { entry } = manifest._; + const { client } = manifest._; - const stylesheets = new Set(entry.stylesheets); - const modulepreloads = new Set(entry.imports); - const fonts = new Set(manifest._.entry.fonts); + const modulepreloads = new Set([...client.start.imports, ...client.app.imports]); + const stylesheets = new Set(client.app.stylesheets); + const fonts = new Set(client.app.fonts); /** @type {Set} */ const link_header_preloads = new Set(); @@ -141,17 +141,9 @@ export async function render_response({ } for (const { node } of branch) { - if (node.imports) { - node.imports.forEach((url) => modulepreloads.add(url)); - } - - if (node.stylesheets) { - node.stylesheets.forEach((url) => stylesheets.add(url)); - } - - if (node.fonts) { - node.fonts.forEach((url) => fonts.add(url)); - } + for (const url of node.imports) modulepreloads.add(url); + for (const url of node.stylesheets) stylesheets.add(url); + for (const url of node.fonts) fonts.add(url); if (node.inline_styles) { Object.entries(await node.inline_styles()).forEach(([k, v]) => inline_styles.set(k, v)); @@ -161,34 +153,52 @@ export async function render_response({ rendered = { head: '', html: '', css: { code: '', map: null } }; } - let head = ''; - let body = rendered.html; - - const csp = new Csp(options.csp, { - prerender: !!state.prerendering - }); - - const target = hash(body); - /** * The prefix to use for static assets. Replaces `%sveltekit.assets%` in the template * @type {string} */ let resolved_assets; + /** + * An expression that will evaluate in the client to determine the resolved asset path + */ + let asset_expression; + if (assets) { // if an asset path is specified, use it resolved_assets = assets; + asset_expression = s(assets); } else if (state.prerendering?.fallback) { // if we're creating a fallback page, asset paths need to be root-relative resolved_assets = base; + asset_expression = s(base); } else { // otherwise we want asset paths to be relative to the page, so that they // will work in odd contexts like IPFS, the internet archive, and so on const segments = event.url.pathname.slice(base.length).split('/').slice(2); resolved_assets = segments.length > 0 ? segments.map(() => '..').join('/') : '.'; + asset_expression = `new URL(${s( + resolved_assets + )}, location.href).pathname.replace(/^\\\/$/, '')`; } + let head = ''; + let body = rendered.html; + + const csp = new Csp(options.csp, { + prerender: !!state.prerendering + }); + + const init = `__sveltekit_${options.version_hash}={env:${s( + public_env + )},assets:${asset_expression}}`; + + csp.add_script(init); + + head += `${init}`; + + const target = hash(body); + /** @param {string} path */ const prefixed = (path) => { if (path.startsWith('/')) { @@ -290,12 +300,7 @@ export async function render_response({ } if (page_config.csr) { - const opts = [ - `assets: ${s(assets)}`, - `env: ${s(public_env)}`, - `target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode`, - `version: ${s(version)}` - ]; + const args = [`app`, `"${target}"`]; if (page_config.ssr) { const hydrate = [ @@ -313,27 +318,28 @@ export async function render_response({ hydrate.push(`params: ${devalue.uneval(event.params)}`, `route: ${s(event.route)}`); } - opts.push(`hydrate: {\n\t\t\t\t\t${hydrate.join(',\n\t\t\t\t\t')}\n\t\t\t\t}`); + args.push(`{\n\t\t\t\t${hydrate.join(',\n\t\t\t\t')}\n\t\t\t}`); } // prettier-ignore const init_app = ` - import { start } from ${s(prefixed(entry.file))}; + import { start } from ${s(prefixed(client.start.file))}; + import * as app from ${s(prefixed(client.app.file))}; - start({ - ${opts.join(',\n\t\t\t\t')} - }); + start(${args.join(', ')}); `; - for (const dep of modulepreloads) { - const path = prefixed(dep); - - if (resolve_opts.preload({ type: 'js', path })) { - link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`); - if (state.prerendering) { - head += `\n\t\t`; - } - } + const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter( + (path) => resolve_opts.preload({ type: 'js', path }) + ); + + for (const path of included_modulepreloads) { + // we use modulepreload with the Link header for Chrome, along with + // for Safari. This results in the fastest loading in + // the most used browsers, with no double-loading. Note that we need to use + // .mjs extensions for `preload` to behave like `modulepreload` in Chrome + link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`); + head += `\n\t\t`; } const attributes = ['type="module"', `data-sveltekit-hydrate="${target}"`]; diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 9a58ee3bf021..176017e1ffc1 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -3,7 +3,7 @@ import { json, text } from '../../exports/index.js'; import { coalesce_to_error } from '../../utils/error.js'; import { negotiate } from '../../utils/http.js'; import { HttpError } from '../control.js'; -import { fix_stack_trace } from '../shared.js'; +import { fix_stack_trace } from '../shared-server.js'; /** @param {any} body */ export function is_pojo(body) { diff --git a/packages/kit/src/runtime/shared.js b/packages/kit/src/runtime/shared-server.js similarity index 73% rename from packages/kit/src/runtime/shared.js rename to packages/kit/src/runtime/shared-server.js index 004412f3ba66..faaaca4cec79 100644 --- a/packages/kit/src/runtime/shared.js +++ b/packages/kit/src/runtime/shared-server.js @@ -1,8 +1,5 @@ export { set_assets } from '__sveltekit/paths'; -export let building = false; -export let version = ''; - /** @type {Record} */ export let private_env = {}; @@ -12,11 +9,6 @@ export let public_env = {}; /** @param {string} stack */ export let fix_stack_trace = (stack) => stack; -/** @param {boolean} value */ -export function set_building(value) { - building = value; -} - /** @type {(environment: Record) => void} */ export function set_private_env(environment) { private_env = environment; @@ -27,11 +19,6 @@ export function set_public_env(environment) { public_env = environment; } -/** @param {string} value */ -export function set_version(value) { - version = value; -} - /** @param {(stack: string) => string} value */ export function set_fix_stack_trace(value) { fix_stack_trace = value; diff --git a/packages/kit/test/apps/basics/src/global.d.ts b/packages/kit/test/apps/basics/src/global.d.ts index 5467772d10ad..cb794387c759 100644 --- a/packages/kit/test/apps/basics/src/global.d.ts +++ b/packages/kit/test/apps/basics/src/global.d.ts @@ -6,6 +6,7 @@ declare global { mounted: number; fulfil_navigation: (value: any) => void; promise: Promise; + PUBLIC_DYNAMIC: string; } } diff --git a/packages/kit/test/apps/basics/src/hooks.client.js b/packages/kit/test/apps/basics/src/hooks.client.js index 09bfd498b0cc..2dbe1020af0a 100644 --- a/packages/kit/test/apps/basics/src/hooks.client.js +++ b/packages/kit/test/apps/basics/src/hooks.client.js @@ -1,3 +1,7 @@ +import { env } from '$env/dynamic/public'; + +window.PUBLIC_DYNAMIC = env.PUBLIC_DYNAMIC; + /** @type{import("@sveltejs/kit").HandleClientError} */ export function handleError({ error, event }) { return event.url.pathname.endsWith('404-fallback') diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 086452215eef..93c3618a983b 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -544,7 +544,7 @@ test.describe('data-sveltekit attributes', () => { const module = process.env.DEV ? `${baseURL}/src/routes/data-sveltekit/preload-data/target/+page.svelte` - : `${baseURL}/_app/immutable/components/pages/data-sveltekit/preload-data/target/_page`; + : `${baseURL}/_app/immutable/entry/data-sveltekit-preload-data-target-page`; await page.goto('/data-sveltekit/preload-data'); await page.locator('#one').dispatchEvent('mousemove'); @@ -636,11 +636,18 @@ test.describe('Content negotiation', () => { }); }); -test.describe('env in app.html', () => { - test('can access public env', async ({ page }) => { +test.describe('env', () => { + test('can access public env in app.html', async ({ page }) => { await page.goto('/'); expect(await page.locator('body').getAttribute('class')).toContain('groovy'); }); + + test('can access public env in hooks.client.js', async ({ page }) => { + await page.goto('/'); + expect(await page.evaluate(() => window.PUBLIC_DYNAMIC)).toBe( + 'accessible anywhere/evaluated at run time' + ); + }); }); test.describe('Snapshots', () => { diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index f9109342f130..7fe016a145bc 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -502,7 +502,7 @@ test.describe('Prefetching', () => { } else { // the preload helper causes an additional request to be made in Firefox, // so we use toBeGreaterThan rather than toBe - expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0); + expect(requests.filter((req) => req.endsWith('.mjs')).length).toBeGreaterThan(0); } expect(requests.includes(`${baseURL}/routing/preloading/preloaded.json`)).toBe(true); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index cd95a60439dc..05bc19a9f641 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -29,7 +29,7 @@ test.describe('Imports', () => { ]); } else { expect(sources[0].startsWith('data:image/png;base64,')).toBeTruthy(); - expect(sources[1]).toBe(`${baseURL}/_app/immutable/assets/large-3183867c.jpg`); + expect(sources[1]).toBe(`${baseURL}/_app/immutable/assets/large.3183867c.jpg`); } }); }); diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js index 76056ef7fa85..f9b7cff16d3a 100644 --- a/packages/kit/test/apps/options-2/test/test.js +++ b/packages/kit/test/apps/options-2/test/test.js @@ -32,7 +32,7 @@ test.describe('Service worker', () => { const response = await request.get('/basepath/service-worker.js'); const content = await response.text(); - expect(content).toMatch(/\/_app\/immutable\/start-[a-z0-9]+\.js/); + expect(content).toMatch(/\/_app\/immutable\/entry\/start\.[a-z0-9]+\.mjs/); }); test('does not register /basepath/service-worker.js', async ({ page }) => { diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index 685c49400774..3746deaea278 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -233,7 +233,7 @@ test.describe('trailingSlash', () => { if (process.env.DEV) { expect(requests.filter((req) => req.endsWith('.svelte')).length).toBe(1); } else { - expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0); + expect(requests.filter((req) => req.endsWith('.mjs')).length).toBeGreaterThan(0); } expect(requests.includes(`/path-base/preloading/preloaded/__data.json`)).toBe(true); @@ -262,7 +262,7 @@ test.describe('trailingSlash', () => { if (process.env.DEV) { expect(requests.filter((req) => req.endsWith('.svelte')).length).toBe(1); } else { - expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0); + expect(requests.filter((req) => req.endsWith('.mjs')).length).toBeGreaterThan(0); } requests = []; diff --git a/packages/kit/test/prerendering/basics/test/test.js b/packages/kit/test/prerendering/basics/test/test.js index d205597f3446..6e5941290adc 100644 --- a/packages/kit/test/prerendering/basics/test/test.js +++ b/packages/kit/test/prerendering/basics/test/test.js @@ -166,11 +166,7 @@ test('targets the data-sveltekit-hydrate parent node', () => { assert.equal(match[1].trim(), '

hello

'); - assert.ok( - match[3].includes( - `target: document.querySelector('[data-sveltekit-hydrate="${match[2]}"]').parentNode` - ) - ); + assert.ok(match[3].includes(`app, "${match[2]}"`)); }); test('prerenders binary data', async () => { diff --git a/packages/kit/test/utils.js b/packages/kit/test/utils.js index e3e517a34f47..64e8d68b3357 100644 --- a/packages/kit/test/utils.js +++ b/packages/kit/test/utils.js @@ -93,7 +93,7 @@ export const test = base.extend({ page[fn] = async function (...args) { const res = await page_fn.call(page, ...args); if (javaScriptEnabled && args[1]?.wait_for_started !== false) { - await page.waitForSelector('body.started', { timeout: 5000 }); + await page.waitForSelector('body.started', { timeout: 15000 }); } return res; }; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 042d5b529ddd..73c5683ca8fe 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -18,7 +18,7 @@ import { RouteSegment, UniqueInterface } from './private.js'; -import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js'; +import { AssetDependencies, SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js'; export { PrerenderOption } from './private.js'; @@ -1008,11 +1008,9 @@ export interface SSRManifest { /** private fields */ _: { - entry: { - file: string; - imports: string[]; - stylesheets: string[]; - fonts: string[]; + client: { + start: AssetDependencies; + app: AssetDependencies; }; nodes: SSRNodeLoader[]; routes: SSRRoute[]; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 8649c3f2bf17..93c62181ccfe 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -42,16 +42,21 @@ export interface Asset { type: string | null; } +export interface AssetDependencies { + file: string; + imports: string[]; + stylesheets: string[]; + fonts: string[]; +} + export interface BuildData { app_dir: string; app_path: string; manifest_data: ManifestData; service_worker: string | null; - client_entry: { - file: string; - imports: string[]; - stylesheets: string[]; - fonts: string[]; + client: { + start: AssetDependencies; + app: AssetDependencies; } | null; server_manifest: import('vite').Manifest; } @@ -90,6 +95,11 @@ export interface ClientHooks { handleError: HandleClientError; } +export interface Env { + private: Record; + public: Record; +} + export class InternalServer extends Server { init(options: ServerInitOptions): Promise; respond( @@ -318,6 +328,7 @@ export interface SSROptions { }): string; error(values: { message: string; status: number }): string; }; + version_hash: string; } export interface SSRErrorPage {