Skip to content

Commit 18d2eed

Browse files
committed
refactor(core): move all rendering and cleanup logic into core
1 parent be4748e commit 18d2eed

File tree

10 files changed

+124
-82
lines changed

10 files changed

+124
-82
lines changed

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export default tseslint.config(
6868
name: 'extras',
6969
rules: {
7070
'unicorn/prevent-abbreviations': 'off',
71+
'unicorn/prefer-dom-node-append': 'off',
7172
},
7273
},
7374
{

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"test:all": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest && pnpm test:examples",
2222
"test:all:legacy": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest",
2323
"build": "pnpm build:types && pnpm build:docs",
24-
"build:types": "tsc --build && cp packages/svelte/src/component-types.d.ts packages/svelte/types",
24+
"build:types": "tsc --build && cp packages/svelte/src/core/types.d.ts packages/svelte/types/core",
2525
"build:docs": "remark --output --use remark-toc --use remark-code-import --use unified-prettier README.md examples && cp -f README.md packages/svelte",
2626
"contributors:add": "all-contributors add",
2727
"contributors:generate": "all-contributors generate",

packages/svelte/src/core/index.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
* Will switch to legacy, class-based mounting logic
66
* if it looks like we're in a Svelte <= 4 environment.
77
*/
8-
export { addCleanupTask, cleanup } from './cleanup.js'
9-
export { mount } from './mount.js'
10-
export {
11-
UnknownSvelteOptionsError,
12-
validateOptions,
13-
} from './validate-options.js'
8+
export * from './cleanup.js'
9+
export * from './mount.js'
10+
export * from './setup.js'

packages/svelte/src/core/mount.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,7 @@ import * as Svelte from 'svelte'
55

66
import { addCleanupTask, removeCleanupTask } from './cleanup.js'
77
import { createProps } from './props.svelte.js'
8-
9-
/** Whether we're using Svelte >= 5. */
10-
const IS_MODERN_SVELTE = typeof Svelte.mount === 'function'
11-
12-
/** Allowed options to the `mount` call or legacy component constructor. */
13-
const ALLOWED_MOUNT_OPTIONS = IS_MODERN_SVELTE
14-
? ['target', 'anchor', 'props', 'events', 'context', 'intro']
15-
: ['target', 'accessors', 'anchor', 'props', 'hydrate', 'intro', 'context']
8+
import { IS_MODERN_SVELTE } from './svelte-version.js'
169

1710
/** Mount a modern Svelte 5 component into the DOM. */
1811
const mountModern = (Component, options) => {
@@ -92,4 +85,4 @@ const mount = (Component, options = {}) => {
9285
}
9386
}
9487

95-
export { ALLOWED_MOUNT_OPTIONS, mount }
88+
export { mount }

packages/svelte/src/core/setup.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/** Set up the document to render a component. */
2+
import { addCleanupTask } from './cleanup.js'
3+
import { IS_MODERN_SVELTE } from './svelte-version.js'
4+
5+
/** Allowed options to the `mount` call or legacy component constructor. */
6+
const ALLOWED_MOUNT_OPTIONS = IS_MODERN_SVELTE
7+
? ['target', 'anchor', 'props', 'events', 'context', 'intro']
8+
: ['target', 'accessors', 'anchor', 'props', 'hydrate', 'intro', 'context']
9+
10+
class UnknownSvelteOptionsError extends TypeError {
11+
constructor(unknownOptions) {
12+
super(`Unknown options.
13+
14+
Unknown: [ ${unknownOptions.join(', ')} ]
15+
Allowed: [ ${ALLOWED_MOUNT_OPTIONS.join(', ')} ]
16+
17+
To pass both Svelte options and props to a component,
18+
or to use props that share a name with a Svelte option,
19+
you must place all your props under the \`props\` key:
20+
21+
render(Component, { props: { /** props here **/ } })
22+
`)
23+
this.name = 'UnknownSvelteOptionsError'
24+
}
25+
}
26+
27+
/**
28+
* Validate a component's mount options.
29+
*
30+
* @template {import('./types.js').Component} C
31+
* @param {import('./types.js').ComponentOptions<C>} options - props or mount options
32+
* @returns {Partial<import('./types.js').MountOptions<C>>}
33+
*/
34+
const validateOptions = (options) => {
35+
const isProps = !Object.keys(options).some((option) =>
36+
ALLOWED_MOUNT_OPTIONS.includes(option)
37+
)
38+
39+
if (isProps) {
40+
return { props: options }
41+
}
42+
43+
// Check if any props and Svelte options were accidentally mixed.
44+
const unknownOptions = Object.keys(options).filter(
45+
(option) => !ALLOWED_MOUNT_OPTIONS.includes(option)
46+
)
47+
48+
if (unknownOptions.length > 0) {
49+
throw new UnknownSvelteOptionsError(unknownOptions, ALLOWED_MOUNT_OPTIONS)
50+
}
51+
52+
return options
53+
}
54+
55+
/**
56+
* Set up the document to render a component.
57+
*
58+
* @template {import('./types.js').Component} C
59+
* @param {import('./types.js').ComponentOptions<C>} options - props or mount options
60+
* @param {{ baseElement?: HTMLElement | undefined }} setupOptions - base element of the document to bind any queries
61+
* @returns {{
62+
* baseElement: HTMLElement,
63+
* target: HTMLElement,
64+
* mountOptions: import('./types.js).MountOptions<C>
65+
* }}
66+
*/
67+
const setup = (options, setupOptions) => {
68+
const mountOptions = validateOptions(options)
69+
const baseElement =
70+
setupOptions.baseElement ?? mountOptions.target ?? document.body
71+
let target = mountOptions.target
72+
73+
if (!target) {
74+
target = baseElement.appendChild(document.createElement('div'))
75+
}
76+
77+
addCleanupTask(() => {
78+
if (target.parentNode === document.body) {
79+
target.remove()
80+
}
81+
})
82+
83+
return { baseElement, target, mountOptions: { ...mountOptions, target } }
84+
}
85+
86+
export { setup, UnknownSvelteOptionsError }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** Detect which version of Svelte we're using */
2+
import * as Svelte from 'svelte'
3+
4+
/** Whether we're using Svelte >= 5. */
5+
const IS_MODERN_SVELTE = typeof Svelte.mount === 'function'
6+
7+
export { IS_MODERN_SVELTE }

packages/svelte/src/component-types.d.ts renamed to packages/svelte/src/core/types.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-deprecated */
2+
/**
3+
* Component and utility types.
4+
*
5+
* Supports components from Svelte 3, 4, and 5.
6+
*/
27
import type {
38
Component as ModernComponent,
49
ComponentConstructorOptions as LegacyConstructorOptions,
@@ -59,3 +64,8 @@ export type Exports<C> = IS_MODERN_SVELTE extends true
5964
export type MountOptions<C extends Component> = IS_MODERN_SVELTE extends true
6065
? Parameters<typeof mount<Props<C>, Exports<C>>>[1]
6166
: LegacyConstructorOptions<Props<C>>
67+
68+
/** A component's props or some of its mount options. */
69+
export type ComponentOptions<C extends Component> =
70+
| Props<C>
71+
| Partial<MountOptions<C>>

packages/svelte/src/core/validate-options.js

Lines changed: 0 additions & 41 deletions
This file was deleted.

packages/svelte/src/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ if (typeof process !== 'undefined' && !process.env.STL_SKIP_AUTO_CLEANUP) {
2323
export * from '@testing-library/dom'
2424

2525
// export svelte-specific functions and custom `fireEvent`
26-
export { UnknownSvelteOptionsError } from './core/index.js'
2726
export * from './pure.js'
2827
// `fireEvent` must be named to take priority over wildcard from @testing-library/dom
2928
export { fireEvent } from './pure.js'

packages/svelte/src/pure.js

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import {
77
} from '@testing-library/dom'
88
import * as Svelte from 'svelte'
99

10-
import { addCleanupTask, mount, validateOptions } from './core/index.js'
10+
import { addCleanupTask, mount, setup as coreSetup } from './core/index.js'
1111

1212
/**
1313
* Customize how Svelte renders the component.
1414
*
15-
* @template {import('./component-types.js').Component} C
16-
* @typedef {import('./component-types.js').Props<C> | Partial<import('./component-types.js').MountOptions<C>>} SvelteComponentOptions
15+
* @template {import('./core/types.js').Component} C
16+
* @typedef {import('./core/types.js').ComponentOptions<C>} SvelteComponentOptions
1717
*/
1818

1919
/**
@@ -29,15 +29,15 @@ import { addCleanupTask, mount, validateOptions } from './core/index.js'
2929
/**
3030
* The rendered component and bound testing functions.
3131
*
32-
* @template {import('./component-types.js').Component} C
32+
* @template {import('./core/types.js').Component} C
3333
* @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
3434
*
3535
* @typedef {{
3636
* container: HTMLElement
3737
* baseElement: HTMLElement
38-
* component: import('./component-types.js').Exports<C>
38+
* component: import('./core/types.js').Exports<C>
3939
* debug: (el?: HTMLElement | DocumentFragment) => void
40-
* rerender: (props: Partial<import('./component-types.js').Props<C>>) => Promise<void>
40+
* rerender: (props: Partial<import('./core/types.js').Props<C>>) => Promise<void>
4141
* unmount: () => void
4242
* } & {
4343
* [P in keyof Q]: import('@testing-library/dom').BoundFunction<Q[P]>
@@ -47,35 +47,25 @@ import { addCleanupTask, mount, validateOptions } from './core/index.js'
4747
/**
4848
* Render a component into the document.
4949
*
50-
* @template {import('./component-types.js').Component} C
50+
* @template {import('./core/types.js').Component} C
5151
* @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
5252
*
53-
* @param {import('./component-types.js').ComponentType<C>} Component - The component to render.
53+
* @param {import('./core/types.js').ComponentType<C>} Component - The component to render.
5454
* @param {SvelteComponentOptions<C>} options - Customize how Svelte renders the component.
5555
* @param {RenderOptions<Q>} renderOptions - Customize how Testing Library sets up the document and binds queries.
5656
* @returns {RenderResult<C, Q>} The rendered component and bound testing functions.
5757
*/
5858
const render = (Component, options = {}, renderOptions = {}) => {
59-
options = validateOptions(options)
60-
61-
const baseElement =
62-
renderOptions.baseElement ?? options.target ?? document.body
59+
const { baseElement, target, mountOptions } = coreSetup(
60+
options,
61+
renderOptions
62+
)
6363

6464
const queries = getQueriesForElement(baseElement, renderOptions.queries)
6565

66-
const target =
67-
// eslint-disable-next-line unicorn/prefer-dom-node-append
68-
options.target ?? baseElement.appendChild(document.createElement('div'))
69-
70-
addCleanupTask(() => {
71-
if (target.parentNode === document.body) {
72-
target.remove()
73-
}
74-
})
75-
7666
const { component, unmount, rerender } = mount(
7767
Component.default ?? Component,
78-
{ ...options, target }
68+
mountOptions
7969
)
8070

8171
return {
@@ -160,5 +150,5 @@ for (const [key, baseEvent] of Object.entries(baseFireEvent)) {
160150
fireEvent[key] = async (...args) => act(() => baseEvent(...args))
161151
}
162152

163-
export { cleanup } from './core/index.js'
153+
export { cleanup, UnknownSvelteOptionsError } from './core/index.js'
164154
export { act, fireEvent, render, setup }

0 commit comments

Comments
 (0)