diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md index c60d43f430..43f785fe33 100644 --- a/documentation/docs/98-reference/.generated/server-errors.md +++ b/documentation/docs/98-reference/.generated/server-errors.md @@ -37,10 +37,10 @@ This error occurs when using `hydratable` or `setHydratableValue` multiple times ``` -### lifecycle_function_unavailable +### render_context_unavailable ``` -`%name%(...)` is not available on the server +Failed to retrieve `render` context. %addendum% ``` -Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render. +If `AsyncLocalStorage` is available, you're likely calling a function that needs access to the `render` context (`hydratable`, `cache`, or something that depends on these) from outside of `render`. If `AsyncLocalStorage` is not available, these functions must also be called synchronously from within `render` -- i.e. not after any `await`s. diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 136b3f4957..ceb622cb30 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -42,6 +42,14 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` ``` +### lifecycle_function_unavailable + +``` +`%name%(...)` is not available on the %location% +``` + +Certain methods such as `mount` cannot be invoked while running in a server context, while others, such as `hydratable.set`, cannot be invoked while running in the browser. + ### lifecycle_outside_component ``` diff --git a/packages/svelte/messages/server-errors/errors.md b/packages/svelte/messages/server-errors/errors.md index 4f2491a25a..d8f40a7908 100644 --- a/packages/svelte/messages/server-errors/errors.md +++ b/packages/svelte/messages/server-errors/errors.md @@ -29,8 +29,8 @@ This error occurs when using `hydratable` or `setHydratableValue` multiple times ``` -## lifecycle_function_unavailable +## render_context_unavailable -> `%name%(...)` is not available on the server +> Failed to retrieve `render` context. %addendum% -Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render. +If `AsyncLocalStorage` is available, you're likely calling a function that needs access to the `render` context (`hydratable`, `cache`, or something that depends on these) from outside of `render`. If `AsyncLocalStorage` is not available, these functions must also be called synchronously from within `render` -- i.e. not after any `await`s. diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index bf053283e4..1265e55383 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -34,6 +34,12 @@ Here, `List.svelte` is using `{@render children(item)` which means it expects `P > A snippet function was passed invalid arguments. Snippets should only be instantiated via `{@render ...}` +## lifecycle_function_unavailable + +> `%name%(...)` is not available on the %location% + +Certain methods such as `mount` cannot be invoked while running in a server context, while others, such as `hydratable.set`, cannot be invoked while running in the browser. + ## lifecycle_outside_component > `%name%(...)` can only be used during component initialisation diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0b94e2b7cc..4e9fe667a4 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -95,10 +95,6 @@ "types": "./types/index.d.ts", "default": "./src/server/index.js" }, - "./client": { - "types": "./types/index.d.ts", - "default": "./src/client/index.js" - }, "./store": { "types": "./types/index.d.ts", "worker": "./src/store/index-server.js", diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index 10d3caafa3..0ee6004d4a 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -45,7 +45,6 @@ await createBundle({ [`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`, [`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`, - [`${pkg.name}/client`]: `${dir}/src/client/index.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, [`${pkg.name}/events`]: `${dir}/src/events/public.d.ts`, diff --git a/packages/svelte/src/client/index.js b/packages/svelte/src/client/index.js deleted file mode 100644 index 1cd028d123..0000000000 --- a/packages/svelte/src/client/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { - get_hydratable_value as getHydratableValue, - has_hydratable_value as hasHydratableValue -} from '../internal/client/hydratable.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 9fb810fd9e..a6141851a1 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -22,19 +22,19 @@ export function createEventDispatcher() { } export function mount() { - e.lifecycle_function_unavailable('mount'); + e.lifecycle_function_unavailable('mount', 'server'); } export function hydrate() { - e.lifecycle_function_unavailable('hydrate'); + e.lifecycle_function_unavailable('hydrate', 'server'); } export function unmount() { - e.lifecycle_function_unavailable('unmount'); + e.lifecycle_function_unavailable('unmount', 'server'); } export function fork() { - e.lifecycle_function_unavailable('fork'); + e.lifecycle_function_unavailable('fork', 'server'); } export async function tick() {} diff --git a/packages/svelte/src/internal/client/hydratable.js b/packages/svelte/src/internal/client/hydratable.js index 4045eb94ce..5ce2efe065 100644 --- a/packages/svelte/src/internal/client/hydratable.js +++ b/packages/svelte/src/internal/client/hydratable.js @@ -1,4 +1,4 @@ -/** @import { Decode, Transport } from '#shared' */ +/** @import { Decode, Hydratable, Transport } from '#shared' */ import { async_mode_flag } from '../flags/index.js'; import { hydrating } from './dom/hydration.js'; import * as w from './warnings.js'; @@ -11,7 +11,7 @@ import * as e from './errors.js'; * @param {Transport} [options] * @returns {T} */ -export function hydratable(key, fn, options) { +function isomorphic_hydratable(key, fn, options) { if (!async_mode_flag) { e.experimental_async_required('hydratable'); } @@ -28,13 +28,22 @@ export function hydratable(key, fn, options) { ); } +isomorphic_hydratable['get'] = get_hydratable_value; +isomorphic_hydratable['has'] = has_hydratable_value; +isomorphic_hydratable['set'] = () => e.lifecycle_function_unavailable('hydratable.set', 'browser'); + +/** @type {Hydratable} */ +const hydratable = isomorphic_hydratable; + +export { hydratable }; + /** * @template T * @param {string} key * @param {{ decode?: Decode }} [options] * @returns {T | undefined} */ -export function get_hydratable_value(key, options = {}) { +function get_hydratable_value(key, options = {}) { if (!async_mode_flag) { e.experimental_async_required('getHydratableValue'); } @@ -50,7 +59,7 @@ export function get_hydratable_value(key, options = {}) { * @param {string} key * @returns {boolean} */ -export function has_hydratable_value(key) { +function has_hydratable_value(key) { if (!async_mode_flag) { e.experimental_async_required('hasHydratableValue'); } diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index d229b44ec0..f3f7ad1f40 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -47,12 +47,12 @@ ${stack}\nhttps://svelte.dev/e/hydratable_clobbering`); } /** - * `%name%(...)` is not available on the server - * @param {string} name + * Failed to retrieve `render` context. %addendum% + * @param {string} addendum * @returns {never} */ -export function lifecycle_function_unavailable(name) { - const error = new Error(`lifecycle_function_unavailable\n\`${name}(...)\` is not available on the server\nhttps://svelte.dev/e/lifecycle_function_unavailable`); +export function render_context_unavailable(addendum) { + const error = new Error(`render_context_unavailable\nFailed to retrieve \`render\` context. ${addendum}\nhttps://svelte.dev/e/render_context_unavailable`); error.name = 'Svelte error'; diff --git a/packages/svelte/src/internal/server/hydratable.js b/packages/svelte/src/internal/server/hydratable.js index 1b70353f40..9b8d20facc 100644 --- a/packages/svelte/src/internal/server/hydratable.js +++ b/packages/svelte/src/internal/server/hydratable.js @@ -1,4 +1,4 @@ -/** @import { Encode, Transport } from '#shared' */ +/** @import { Encode, Hydratable, Transport } from '#shared' */ /** @import { HydratableEntry } from '#server' */ import { async_mode_flag } from '../flags/index.js'; @@ -13,7 +13,7 @@ import { DEV } from 'esm-env'; * @param {Transport} [options] * @returns {T} */ -export function hydratable(key, fn, options) { +function isomorphic_hydratable(key, fn, options) { if (!async_mode_flag) { e.experimental_async_required('hydratable'); } @@ -28,13 +28,23 @@ export function hydratable(key, fn, options) { store.hydratables.set(key, entry); return entry.value; } + +isomorphic_hydratable['get'] = () => e.lifecycle_function_unavailable('hydratable.get', 'server'); +isomorphic_hydratable['has'] = has_hydratable_value; +isomorphic_hydratable['set'] = set_hydratable_value; + +/** @type {Hydratable} */ +const hydratable = isomorphic_hydratable; + +export { hydratable }; + /** * @template T * @param {string} key * @param {T} value * @param {{ encode?: Encode }} [options] */ -export function set_hydratable_value(key, value, options = {}) { +function set_hydratable_value(key, value, options = {}) { if (!async_mode_flag) { e.experimental_async_required('setHydratableValue'); } @@ -48,6 +58,18 @@ export function set_hydratable_value(key, value, options = {}) { store.hydratables.set(key, create_entry(value, options?.encode)); } +/** + * @param {string} key + * @returns {boolean} + */ +function has_hydratable_value(key) { + if (!async_mode_flag) { + e.experimental_async_required('hasHydratableValue'); + } + const store = get_render_context(); + return store.hydratables.has(key); +} + /** * @template T * @param {T} value diff --git a/packages/svelte/src/internal/server/render-context.js b/packages/svelte/src/internal/server/render-context.js index 66fe3bb5b8..4ec0274693 100644 --- a/packages/svelte/src/internal/server/render-context.js +++ b/packages/svelte/src/internal/server/render-context.js @@ -3,6 +3,7 @@ /** @import { RenderContext } from '#server' */ import { deferred } from '../shared/utils.js'; +import * as e from './errors.js'; /** @type {Promise | null} */ let current_render = null; @@ -38,18 +39,9 @@ export function get_render_context() { const store = try_get_render_context(); if (!store) { - // TODO make this a proper e.error - let message = 'Could not get rendering context.'; - - if (als) { - message += ' You may have called `hydratable` or `cache` outside of the render lifecycle.'; - } else { - message += - ' In environments without `AsyncLocalStorage`, `hydratable` must be accessed synchronously, not after an `await`.' + - ' If it was accessed synchronously then this is an internal error or you may have called `hydratable` or `cache` outside of the render lifecycle.'; - } - - throw new Error(message); + e.render_context_unavailable( + `\`AsyncLocalStorage\` is ${als ? 'available' : 'not available'}.` + ); } return store; @@ -93,6 +85,7 @@ export async function init_render_context() { } catch {} } +// this has to be a function because rollup won't treeshake it if it's a constant function in_webcontainer() { // @ts-ignore -- this will fail when we run typecheck because we exclude node types // eslint-disable-next-line n/prefer-global/process diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 7162253ce1..4fa79eedea 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -576,12 +576,12 @@ export class Renderer { async #collect_hydratables() { const map = get_render_context().hydratables; /** @type {(value: unknown) => string} */ - let default_stringify; + const default_encode = new MemoizedUneval().uneval; /** @type {[string, unknown][]} */ let entries = []; for (const [k, v] of map) { - const encode = v.encode ?? (default_stringify ??= new MemoizedUneval().uneval); + const encode = v.encode ?? default_encode; // sequential await is okay here -- all the work is already kicked off entries.push([k, encode(await v.value)]); } @@ -649,9 +649,7 @@ export class Renderer { for (const [k, v] of serialized) { entries.push(`["${k}",${v}]`); } - // TODO csp? - // TODO how can we communicate this error better? Is there a way to not just send it to the console? - // (it is probably very rare so... not too worried) + // TODO csp -- have discussed but not implemented return `