From 3da2646b10733b9d801830cba32c87ef5e7fbad4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Jun 2024 07:57:10 -0700 Subject: [PATCH] feat: more accurate `render`/`mount`/`hydrate` options (#12111) make the props parameter optional only when there are no or only optional props --------- Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/famous-chairs-notice.md | 5 ++ packages/svelte/scripts/generate-types.js | 2 +- packages/svelte/src/internal/client/render.js | 18 ++++++- packages/svelte/src/internal/server/index.js | 4 +- packages/svelte/src/server/index.d.ts | 21 ++++++++ packages/svelte/tests/types/component.ts | 53 ++++++++++++++++++- packages/svelte/tsconfig.json | 2 +- packages/svelte/types/index.d.ts | 39 +++++++++++--- playgrounds/demo/src/entry-server.ts | 4 +- 9 files changed, 130 insertions(+), 18 deletions(-) create mode 100644 .changeset/famous-chairs-notice.md create mode 100644 packages/svelte/src/server/index.d.ts diff --git a/.changeset/famous-chairs-notice.md b/.changeset/famous-chairs-notice.md new file mode 100644 index 0000000000..a060a9b6a8 --- /dev/null +++ b/.changeset/famous-chairs-notice.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: more accurate `render`/`mount`/`hydrate` options diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index bb195f5f82..b401530f1d 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -30,7 +30,7 @@ await createBundle({ [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, [`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`, [`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`, - [`${pkg.name}/server`]: `${dir}/src/server/index.js`, + [`${pkg.name}/server`]: `${dir}/src/server/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/index.js`, diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index f380f3729f..e1b385cd21 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -72,13 +72,20 @@ export function slot(anchor, slot_fn, slot_props, fallback_fn) { * @template {Record} Props * @template {Record} Exports * @param {import('../../index.js').ComponentType> | import('../../index.js').Component} component - * @param {{ + * @param {{} extends Props ? { * target: Document | Element | ShadowRoot; * anchor?: Node; * props?: Props; * events?: Record any>; * context?: Map; * intro?: boolean; + * }: { + * target: Document | Element | ShadowRoot; + * props: Props; + * anchor?: Node; + * events?: Record any>; + * context?: Map; + * intro?: boolean; * }} options * @returns {Exports} */ @@ -98,13 +105,20 @@ export function mount(component, options) { * @template {Record} Props * @template {Record} Exports * @param {import('../../index.js').ComponentType> | import('../../index.js').Component} component - * @param {{ + * @param {{} extends Props ? { * target: Document | Element | ShadowRoot; * props?: Props; * events?: Record any>; * context?: Map; * intro?: boolean; * recover?: boolean; + * } : { + * target: Document | Element | ShadowRoot; + * props: Props; + * events?: Record any>; + * context?: Map; + * intro?: boolean; + * recover?: boolean; * }} options * @returns {Exports} */ diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b0cf9b19c2..101bdfb90f 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -97,10 +97,10 @@ export let on_destroy = []; * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {import('svelte').Component | import('svelte').ComponentType>} component - * @param {{ props: Omit; context?: Map }} options + * @param {{ props?: Omit; context?: Map }} [options] * @returns {import('#server').RenderOutput} */ -export function render(component, options) { +export function render(component, options = {}) { const payload = create_payload(); const prev_on_destroy = on_destroy; diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts new file mode 100644 index 0000000000..b65ce5bdaa --- /dev/null +++ b/packages/svelte/src/server/index.d.ts @@ -0,0 +1,21 @@ +import type { RenderOutput } from '#server'; +import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte'; + +/** + * Only available on the server and when compiling with the `server` option. + * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. + */ +export function render< + Comp extends SvelteComponent | Component, + Props extends ComponentProps = ComponentProps +>( + ...args: {} extends Props + ? [ + component: Comp extends SvelteComponent ? ComponentType : Comp, + options?: { props?: Omit; context?: Map } + ] + : [ + component: Comp extends SvelteComponent ? ComponentType : Comp, + options: { props: Omit; context?: Map } + ] +): RenderOutput; diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index ca520656fd..fc80653796 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -131,6 +131,13 @@ mount(NewComponent, { intro: false, recover: false }); +mount( + NewComponent, + // @ts-expect-error props missing + { target: null as any } +); +// if component receives no args, props can be omitted +mount(null as any as typeof SvelteComponent<{}>, { target: null as any }); hydrate(NewComponent, { target: null as any as Document | Element | ShadowRoot, @@ -148,6 +155,13 @@ hydrate(NewComponent, { intro: false, recover: false }); +hydrate( + NewComponent, + // @ts-expect-error props missing + { target: null as any } +); +// if component receives no args, props can be omitted +hydrate(null as any as typeof SvelteComponent<{}>, { target: null as any }); render(NewComponent, { props: { @@ -156,6 +170,14 @@ render(NewComponent, { x: '' } }); +// @ts-expect-error +render(NewComponent); +render(NewComponent, { + props: { + // @ts-expect-error + prop: 1 + } +}); // --------------------------------------------------------------------------- interop @@ -255,6 +277,13 @@ mount(functionComponent, { readonly: 1 } }); +mount( + functionComponent, + // @ts-expect-error props missing + { target: null as any } +); +// if component receives no args, props can be omitted +mount(null as any as Component<{}>, { target: null as any }); hydrate(functionComponent, { target: null as any as Document | Element | ShadowRoot, @@ -272,13 +301,33 @@ hydrate(functionComponent, { binding: true } }); +hydrate( + functionComponent, + // @ts-expect-error props missing + { target: null as any } +); +// if component receives no args, props can be omitted +hydrate(null as any as Component<{}>, { target: null as any }); render(functionComponent, { props: { binding: true, - readonly: 'foo', + readonly: 'foo' + } +}); +// @ts-expect-error +render(functionComponent); +render(functionComponent, { + // @ts-expect-error + props: { + binding: true + } +}); +render(functionComponent, { + props: { + binding: true, // @ts-expect-error - x: '' + readonly: 1 } }); diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index 5cb26aef5a..0cc600dc74 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -23,7 +23,7 @@ "svelte/internal/client": ["./src/internal/client/index.js"], "svelte/legacy": ["./src/legacy/legacy-client.js"], "svelte/motion": ["./src/motion/public.d.ts"], - "svelte/server": ["./src/server/index.js"], + "svelte/server": ["./src/server/index.d.ts"], "svelte/store": ["./src/store/public.d.ts"], "#compiler": ["./src/compiler/types/index.d.ts"], "#client": ["./src/internal/client/types.d.ts"], diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 658264418e..6aa73b695a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -350,25 +350,39 @@ declare module 'svelte' { * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ - export function mount, Exports extends Record>(component: ComponentType> | Component, options: { + export function mount, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; anchor?: Node | undefined; props?: Props | undefined; events?: Record any> | undefined; context?: Map | undefined; intro?: boolean | undefined; + } : { + target: Document | Element | ShadowRoot; + props: Props; + anchor?: Node | undefined; + events?: Record any> | undefined; + context?: Map | undefined; + intro?: boolean | undefined; }): Exports; /** * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ - export function hydrate, Exports extends Record>(component: ComponentType> | Component, options: { + export function hydrate, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; props?: Props | undefined; events?: Record any> | undefined; context?: Map | undefined; intro?: boolean | undefined; recover?: boolean | undefined; + } : { + target: Document | Element | ShadowRoot; + props: Props; + events?: Record any> | undefined; + context?: Map | undefined; + intro?: boolean | undefined; + recover?: boolean | undefined; }): Exports; /** * Unmounts a component that was previously mounted using `mount` or `hydrate`. @@ -2110,14 +2124,25 @@ declare module 'svelte/reactivity' { } declare module 'svelte/server' { + import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte'; /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. - * */ - export function render>(component: import("svelte").Component | import("svelte").ComponentType>, options: { - props: Omit; - context?: Map | undefined; - }): RenderOutput; + */ + export function render< + Comp extends SvelteComponent | Component, + Props extends ComponentProps = ComponentProps + >( + ...args: {} extends Props + ? [ + component: Comp extends SvelteComponent ? ComponentType : Comp, + options?: { props?: Omit; context?: Map } + ] + : [ + component: Comp extends SvelteComponent ? ComponentType : Comp, + options: { props: Omit; context?: Map } + ] + ): RenderOutput; interface RenderOutput { /** HTML that goes into the `` */ head: string; diff --git a/playgrounds/demo/src/entry-server.ts b/playgrounds/demo/src/entry-server.ts index d02b5a641b..d4741d2f15 100644 --- a/playgrounds/demo/src/entry-server.ts +++ b/playgrounds/demo/src/entry-server.ts @@ -1,7 +1,5 @@ -// @ts-ignore import { render } from 'svelte/server'; // @ts-ignore you need to create this file import App from './App.svelte'; -// @ts-ignore -export const { head, body, css } = render(App, { props: { initialCount: 0 } }); +export const { head, body } = render(App);