@testing-library/svelte-core 1.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/package.json +53 -0
- package/src/cleanup.js +32 -0
- package/src/index.js +11 -0
- package/src/mount.js +108 -0
- package/src/props.svelte.js +37 -0
- package/src/render.js +21 -0
- package/src/setup.js +86 -0
- package/src/svelte-version.js +7 -0
- package/types.d.ts +131 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Ben Monro <ben.monro@gmail.com> (https://github.com/benmonro)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @testing-library/svelte-core
|
|
2
|
+
|
|
3
|
+
Do you want to build your own Svelte testing library? You may want to use our
|
|
4
|
+
rendering core, which abstracts away differences in Svelte versions to provide a
|
|
5
|
+
simple API to render Svelte components into the document and clean them up
|
|
6
|
+
afterwards
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
- [Example Usage](#example-usage)
|
|
11
|
+
- [API](#api)
|
|
12
|
+
- [`render`](#render)
|
|
13
|
+
- [`setup`](#setup)
|
|
14
|
+
- [`mount`](#mount)
|
|
15
|
+
- [`cleanup`](#cleanup)
|
|
16
|
+
- [`addCleanupTask`](#addcleanuptask)
|
|
17
|
+
- [`removeCleanupTask`](#removecleanuptask)
|
|
18
|
+
- [Utility types](#utility-types)
|
|
19
|
+
|
|
20
|
+
## Example Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { beforeEach } from 'vitest'
|
|
24
|
+
import * as SvelteCore from '@testing-library/svelte-core'
|
|
25
|
+
|
|
26
|
+
import { bindQueries, type Screen } from './bring-your-own-queries.js'
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
SvelteCore.cleanup()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export interface RenderResult<
|
|
33
|
+
C extends SvelteCore.Component,
|
|
34
|
+
> extends SvelteCore.RenderResult<C> {
|
|
35
|
+
screen: Screen
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const render = <C extends SvelteCore.Component>(
|
|
39
|
+
Component: SvelteCore.ComponentImport<C>,
|
|
40
|
+
options: SvelteCore.ComponentOptions<C>
|
|
41
|
+
): RenderResult<C> => {
|
|
42
|
+
const renderResult = SvelteCore.render(Component, options)
|
|
43
|
+
const screen = bindQueries(baseElement)
|
|
44
|
+
|
|
45
|
+
return { screen, ...renderResult }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### `render`
|
|
52
|
+
|
|
53
|
+
Set up the document and mount a component into that document.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const { baseElement, container, component, unmount, rerender } = render(
|
|
57
|
+
Component,
|
|
58
|
+
componentOptions,
|
|
59
|
+
setupOptions
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Argument | Type | Description |
|
|
64
|
+
| ------------------ | ------------------------------------------------------- | --------------------------------------------- |
|
|
65
|
+
| `Component` | [Svelte component][svelte-component-docs] | An imported Svelte component |
|
|
66
|
+
| `componentOptions` | `Props` or partial [`mount` options][svelte-mount-docs] | Options for how the component will be mounted |
|
|
67
|
+
| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` |
|
|
68
|
+
|
|
69
|
+
| Result | Type | Description | Default |
|
|
70
|
+
| ------------- | ------------------------------------------ | ---------------------------------------- | ----------------------------------- |
|
|
71
|
+
| `baseElement` | `HTMLElement` | The base element | `document.body` |
|
|
72
|
+
| `container` | `HTMLElement` | The component's immediate parent element | `<div>` appended to `document.body` |
|
|
73
|
+
| `component` | [component exports][svelte-mount-docs] | The component's exports from `mount` | N/A |
|
|
74
|
+
| `rerender` | `(props: Partial<Props>) => Promise<void>` | Update the component's props | N/A |
|
|
75
|
+
| `unmount` | `() => void` | Unmount the component from the document | N/A |
|
|
76
|
+
|
|
77
|
+
> \[!TIP]
|
|
78
|
+
> Calling `render` is equivalent to calling `setup` followed by `mount`
|
|
79
|
+
>
|
|
80
|
+
> ```ts
|
|
81
|
+
> const { baseElement, container, mountOptions } = setup(
|
|
82
|
+
> componentOptions,
|
|
83
|
+
> setupOptions
|
|
84
|
+
> )
|
|
85
|
+
> const { component, rerender, unmount } = mount(Component, mountOptions)
|
|
86
|
+
> ```
|
|
87
|
+
|
|
88
|
+
[svelte-component-docs]: https://svelte.dev/docs/svelte-components
|
|
89
|
+
[svelte-mount-docs]: https://svelte.dev/docs/svelte/imperative-component-api#mount
|
|
90
|
+
|
|
91
|
+
### `setup`
|
|
92
|
+
|
|
93
|
+
Validate options and prepare document elements for rendering.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const { baseElement, target, mountOptions } = setup(options, renderOptions)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Argument | Type | Description |
|
|
100
|
+
| ------------------ | ------------------------------------------------------- | --------------------------------------------- |
|
|
101
|
+
| `componentOptions` | `Props` or partial [`mount` options][svelte-mount-docs] | Options for how the component will be mounted |
|
|
102
|
+
| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` |
|
|
103
|
+
|
|
104
|
+
| Result | Type | Description | Default |
|
|
105
|
+
| -------------- | ------------------------------------ | ---------------------------------------- | ----------------------------------- |
|
|
106
|
+
| `baseElement` | `HTMLElement` | The base element | `document.body` |
|
|
107
|
+
| `container` | `HTMLElement` | The component's immediate parent element | `<div>` appended to `document.body` |
|
|
108
|
+
| `mountOptions` | [`mount` options][svelte-mount-docs] | Validated options to pass to `mount` | `{ target, props: {} }` |
|
|
109
|
+
|
|
110
|
+
### `mount`
|
|
111
|
+
|
|
112
|
+
Mount a Svelte component into the document.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const { component, unmount, rerender } = mount(Component, options)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| Argument | Type | Description |
|
|
119
|
+
| -------------- | ----------------------------------------- | -------------------------------------------- |
|
|
120
|
+
| `Component` | [Svelte component][svelte-component-docs] | An imported Svelte component |
|
|
121
|
+
| `mountOptions` | [component options][svelte-mount-docs] | Options to pass to Svelte's `mount` function |
|
|
122
|
+
|
|
123
|
+
| Result | Type | Description |
|
|
124
|
+
| ----------- | ------------------------------------------ | --------------------------------------- |
|
|
125
|
+
| `component` | [component exports][svelte-mount-docs] | The component's exports from `mount` |
|
|
126
|
+
| `unmount` | `() => void` | Unmount the component from the document |
|
|
127
|
+
| `rerender` | `(props: Partial<Props>) => Promise<void>` | Update the component's props |
|
|
128
|
+
|
|
129
|
+
### `cleanup`
|
|
130
|
+
|
|
131
|
+
Cleanup rendered components and added elements. Call this when your tests are
|
|
132
|
+
over.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
cleanup()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `addCleanupTask`
|
|
139
|
+
|
|
140
|
+
Add a custom cleanup task to be called with `cleanup()`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
addCleanupTask(() => {
|
|
144
|
+
// ...reset something
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `removeCleanupTask`
|
|
149
|
+
|
|
150
|
+
Remove a cleanup task from `cleanup()`. Useful if a cleanup task can only be run
|
|
151
|
+
once and may be run outside of `cleanup`
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const customCleanup = () => {
|
|
155
|
+
// ...reset something
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
addCleanupTask(customCleanup)
|
|
159
|
+
|
|
160
|
+
const manuallyCleanupEarly = () => {
|
|
161
|
+
customCleanup()
|
|
162
|
+
removeCleanupTask(customCleanup)
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Utility types
|
|
167
|
+
|
|
168
|
+
This module exports various utility types from
|
|
169
|
+
`@testing-library/svelte-core/types`. They adapt to whatever Svelte version is
|
|
170
|
+
installed, and can be used to get type signatures for imported components,
|
|
171
|
+
props, exports, etc.
|
|
172
|
+
|
|
173
|
+
See [`./types.d.ts`](./types.d.ts) for the full list of available types.
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@testing-library/svelte-core",
|
|
3
|
+
"version": "1.0.0-next.1",
|
|
4
|
+
"description": "Core rendering and cleanup logic for Svelte testing utilities.",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"svelte": "./src/index.js",
|
|
9
|
+
"default": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./types": {
|
|
12
|
+
"types": "./types.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/testing-library/svelte-testing-library#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/testing-library/svelte-testing-library.git",
|
|
21
|
+
"directory": "packages/svelte-core"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/testing-library/svelte-testing-library/issues"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=16"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"testing",
|
|
31
|
+
"svelte",
|
|
32
|
+
"ui",
|
|
33
|
+
"dom",
|
|
34
|
+
"jsdom",
|
|
35
|
+
"unit",
|
|
36
|
+
"integration",
|
|
37
|
+
"functional",
|
|
38
|
+
"end-to-end",
|
|
39
|
+
"e2e"
|
|
40
|
+
],
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"src",
|
|
44
|
+
"types.d.ts"
|
|
45
|
+
],
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"provenance": true
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/cleanup.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @type {Set<() => void>} */
|
|
2
|
+
const cleanupTasks = new Set()
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register later cleanup task
|
|
6
|
+
*
|
|
7
|
+
* @param {() => void} onCleanup
|
|
8
|
+
*/
|
|
9
|
+
const addCleanupTask = (onCleanup) => {
|
|
10
|
+
cleanupTasks.add(onCleanup)
|
|
11
|
+
return onCleanup
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Remove a cleanup task without running it.
|
|
16
|
+
*
|
|
17
|
+
* @param {() => void} onCleanup
|
|
18
|
+
*/
|
|
19
|
+
const removeCleanupTask = (onCleanup) => {
|
|
20
|
+
cleanupTasks.delete(onCleanup)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Clean up all components and elements added to the document. */
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
for (const handleCleanup of cleanupTasks.values()) {
|
|
26
|
+
handleCleanup()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
cleanupTasks.clear()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { addCleanupTask, cleanup, removeCleanupTask }
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering core for svelte-testing-library.
|
|
3
|
+
*
|
|
4
|
+
* Defines how components are added to and removed from the DOM.
|
|
5
|
+
* Will switch to legacy, class-based mounting logic
|
|
6
|
+
* if it looks like we're in a Svelte <= 4 environment.
|
|
7
|
+
*/
|
|
8
|
+
export * from './cleanup.js'
|
|
9
|
+
export * from './mount.js'
|
|
10
|
+
export * from './render.js'
|
|
11
|
+
export * from './setup.js'
|
package/src/mount.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component rendering core, with support for Svelte 3, 4, and 5
|
|
3
|
+
*/
|
|
4
|
+
import * as Svelte from 'svelte'
|
|
5
|
+
|
|
6
|
+
import { addCleanupTask, removeCleanupTask } from './cleanup.js'
|
|
7
|
+
import { createProps } from './props.svelte.js'
|
|
8
|
+
import { IS_MODERN_SVELTE } from './svelte-version.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mount a modern Svelte 5 component into the DOM.
|
|
12
|
+
*
|
|
13
|
+
* @template {import('../types.js').Component} C
|
|
14
|
+
* @param {import('../types.js').ComponentType<C>} Component
|
|
15
|
+
* @param {import('../types.js').MountOptions<C>} options
|
|
16
|
+
* @returns {import('../types.js').MountResult<C>}
|
|
17
|
+
*/
|
|
18
|
+
const mountModern = (Component, options) => {
|
|
19
|
+
const [props, updateProps] = createProps(options.props)
|
|
20
|
+
const component = Svelte.mount(Component, { ...options, props })
|
|
21
|
+
|
|
22
|
+
/** Remove the component from the DOM. */
|
|
23
|
+
const unmount = () => {
|
|
24
|
+
Svelte.flushSync(() => Svelte.unmount(component))
|
|
25
|
+
removeCleanupTask(unmount)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Update the component's props. */
|
|
29
|
+
const rerender = (nextProps) => {
|
|
30
|
+
Svelte.flushSync(() => updateProps(nextProps))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addCleanupTask(unmount)
|
|
34
|
+
Svelte.flushSync()
|
|
35
|
+
|
|
36
|
+
return { component, unmount, rerender }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mount a legacy Svelte 3 or 4 component into the DOM.
|
|
41
|
+
*
|
|
42
|
+
* @template {import('../types.js').LegacyComponent} C
|
|
43
|
+
* @param {import('../types.js').ComponentType<C>} Component
|
|
44
|
+
* @param {import('../types.js').MountOptions<C>} options
|
|
45
|
+
* @returns {import('../types.js').MountResult<C>}
|
|
46
|
+
*/
|
|
47
|
+
const mountLegacy = (Component, options) => {
|
|
48
|
+
const component = new Component(options)
|
|
49
|
+
|
|
50
|
+
/** Remove the component from the DOM. */
|
|
51
|
+
const unmount = () => {
|
|
52
|
+
component.$destroy()
|
|
53
|
+
removeCleanupTask(unmount)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Update the component's props. */
|
|
57
|
+
const rerender = (nextProps) => {
|
|
58
|
+
component.$set(nextProps)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// This `$$.on_destroy` listener is included for strict backwards compatibility
|
|
62
|
+
// with previous versions of `@testing-library/svelte`.
|
|
63
|
+
// It's unnecessary and will be removed in a future major version.
|
|
64
|
+
component.$$.on_destroy.push(() => {
|
|
65
|
+
removeCleanupTask(unmount)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
addCleanupTask(unmount)
|
|
69
|
+
|
|
70
|
+
return { component, unmount, rerender }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The mount method in use. */
|
|
74
|
+
const mountComponent = IS_MODERN_SVELTE ? mountModern : mountLegacy
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Render a Svelte component into the document.
|
|
78
|
+
*
|
|
79
|
+
* @template {import('../types.js').Component} C
|
|
80
|
+
* @param {import('../types.js').ComponentImport<C>} Component
|
|
81
|
+
* @param {import('../types.js').MountOptions<C>} options
|
|
82
|
+
* @returns {import('../types.js').MountResult<C>}
|
|
83
|
+
*/
|
|
84
|
+
const mount = (Component, options) => {
|
|
85
|
+
const { component, unmount, rerender } = mountComponent(
|
|
86
|
+
'default' in Component ? Component.default : Component,
|
|
87
|
+
options
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
component,
|
|
92
|
+
unmount,
|
|
93
|
+
rerender: async (props) => {
|
|
94
|
+
if ('props' in props) {
|
|
95
|
+
console.warn(
|
|
96
|
+
'rerender({ props: { ... } }) deprecated, use rerender({ ... }) instead'
|
|
97
|
+
)
|
|
98
|
+
props = props.props
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
rerender(props)
|
|
102
|
+
// Await the next tick for Svelte 3/4, which cannot flush changes synchronously
|
|
103
|
+
await Svelte.tick()
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { mount }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a shallowly reactive props object.
|
|
3
|
+
*
|
|
4
|
+
* This allows us to update props on `rerender`
|
|
5
|
+
* without turing `props` into a deep set of Proxy objects
|
|
6
|
+
*
|
|
7
|
+
* @template {Record<string, unknown>} Props
|
|
8
|
+
* @param {Props} initialProps
|
|
9
|
+
* @returns {[Props, (nextProps: Partial<Props>) => void]}
|
|
10
|
+
*/
|
|
11
|
+
const createProps = (initialProps = {}) => {
|
|
12
|
+
let currentProps = $state.raw(initialProps)
|
|
13
|
+
|
|
14
|
+
const props = new Proxy(initialProps, {
|
|
15
|
+
get(_, key) {
|
|
16
|
+
return currentProps[key]
|
|
17
|
+
},
|
|
18
|
+
set(_, key, value) {
|
|
19
|
+
currentProps[key] = value
|
|
20
|
+
return true
|
|
21
|
+
},
|
|
22
|
+
has(_, key) {
|
|
23
|
+
return Reflect.has(currentProps, key)
|
|
24
|
+
},
|
|
25
|
+
ownKeys() {
|
|
26
|
+
return Reflect.ownKeys(currentProps)
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const update = (nextProps) => {
|
|
31
|
+
currentProps = { ...currentProps, ...nextProps }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [props, update]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { createProps }
|
package/src/render.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { mount } from './mount.js'
|
|
2
|
+
import { setup } from './setup.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render a component into the document.
|
|
6
|
+
*
|
|
7
|
+
* @template {import('../types.js').Component} C
|
|
8
|
+
*
|
|
9
|
+
* @param {import('../types.js').ComponentImport<C>} Component - The component to render.
|
|
10
|
+
* @param {import('../types.js').ComponentOptions<C>} componentOptions - Customize how Svelte renders the component.
|
|
11
|
+
* @param {import('../types.js').SetupOptions<C>} setupOptions - Customize how the document is set up.
|
|
12
|
+
* @returns {import('../types.js').RenderResult<C>} The rendered component.
|
|
13
|
+
*/
|
|
14
|
+
const render = (Component, componentOptions, setupOptions = {}) => {
|
|
15
|
+
const { mountOptions, ...setupResult } = setup(componentOptions, setupOptions)
|
|
16
|
+
const mountResult = mount(Component, mountOptions)
|
|
17
|
+
|
|
18
|
+
return { ...setupResult, ...mountResult }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { render }
|
package/src/setup.js
ADDED
|
@@ -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)
|
|
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>} componentOptions - props or mount options
|
|
60
|
+
* @param {import('../types.js').SetupOptions<C>} setupOptions - base element of the document to bind any queries
|
|
61
|
+
* @returns {import('../types.js').SetupResult<C>}
|
|
62
|
+
*/
|
|
63
|
+
const setup = (componentOptions, setupOptions = {}) => {
|
|
64
|
+
const mountOptions = validateOptions(componentOptions)
|
|
65
|
+
|
|
66
|
+
const baseElement =
|
|
67
|
+
setupOptions.baseElement ?? mountOptions.target ?? document.body
|
|
68
|
+
|
|
69
|
+
const container =
|
|
70
|
+
mountOptions.target ??
|
|
71
|
+
baseElement.appendChild(document.createElement('div'))
|
|
72
|
+
|
|
73
|
+
addCleanupTask(() => {
|
|
74
|
+
if (container.parentNode === document.body) {
|
|
75
|
+
container.remove()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
baseElement,
|
|
81
|
+
container,
|
|
82
|
+
mountOptions: { ...mountOptions, target: container },
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { setup, UnknownSvelteOptionsError }
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* 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
|
+
*/
|
|
7
|
+
import type {
|
|
8
|
+
Component as ModernComponent,
|
|
9
|
+
ComponentConstructorOptions as LegacyConstructorOptions,
|
|
10
|
+
ComponentProps,
|
|
11
|
+
EventDispatcher,
|
|
12
|
+
mount,
|
|
13
|
+
SvelteComponent as Svelte4LegacyComponent,
|
|
14
|
+
SvelteComponentTyped as Svelte3LegacyComponent,
|
|
15
|
+
} from 'svelte'
|
|
16
|
+
|
|
17
|
+
type IS_MODERN_SVELTE = ModernComponent extends (...args: any[]) => any
|
|
18
|
+
? true
|
|
19
|
+
: false
|
|
20
|
+
|
|
21
|
+
type IS_LEGACY_SVELTE_4 =
|
|
22
|
+
EventDispatcher<any> extends (...args: any[]) => any ? true : false
|
|
23
|
+
|
|
24
|
+
/** A compiled, imported Svelte component. */
|
|
25
|
+
export type Component<
|
|
26
|
+
P extends Record<string, any> = any,
|
|
27
|
+
E extends Record<string, any> = any,
|
|
28
|
+
> = IS_MODERN_SVELTE extends true
|
|
29
|
+
? ModernComponent<P, E> | LegacyComponent<P>
|
|
30
|
+
: LegacyComponent<P>
|
|
31
|
+
|
|
32
|
+
/** A compiled, imported Svelte 3 or 4 component. */
|
|
33
|
+
export type LegacyComponent<P extends Record<string, any> = any> =
|
|
34
|
+
IS_LEGACY_SVELTE_4 extends true
|
|
35
|
+
? Svelte4LegacyComponent<P>
|
|
36
|
+
: Svelte3LegacyComponent<P>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The "type" of an imported, compiled Svelte component.
|
|
40
|
+
*
|
|
41
|
+
* In Svelte 5, there is no difference between the
|
|
42
|
+
* imported component and its "type" - it's just a function.
|
|
43
|
+
* In Svelte 3/4, the imported component is a class.
|
|
44
|
+
*/
|
|
45
|
+
export type ComponentType<C> = C extends LegacyComponent
|
|
46
|
+
? new (...args: any[]) => C
|
|
47
|
+
: C
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A component import.
|
|
51
|
+
*
|
|
52
|
+
* A convenience type to allow dynamically `import(...)`'d
|
|
53
|
+
* components to be passed directly to `mount.`
|
|
54
|
+
*/
|
|
55
|
+
export type ComponentImport<C> =
|
|
56
|
+
| ComponentType<C>
|
|
57
|
+
| { default: ComponentType<C> }
|
|
58
|
+
|
|
59
|
+
/** The props of a component. */
|
|
60
|
+
export type Props<C extends Component> = ComponentProps<C>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The exported fields of a component.
|
|
64
|
+
*
|
|
65
|
+
* In Svelte 5, this is the set of variables marked as `export`'d.
|
|
66
|
+
* In Svelte 4, this is simply the instance of the component class.
|
|
67
|
+
*/
|
|
68
|
+
export type Exports<C> = IS_MODERN_SVELTE extends true
|
|
69
|
+
? C extends ModernComponent<any, infer E>
|
|
70
|
+
? E
|
|
71
|
+
: C & { $set: never; $on: never; $destroy: never }
|
|
72
|
+
: C
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Options that may be passed to `mount` when rendering the component.
|
|
76
|
+
*
|
|
77
|
+
* In Svelte 4, these are the options passed to the component constructor.
|
|
78
|
+
*/
|
|
79
|
+
export type MountOptions<C extends Component> = IS_MODERN_SVELTE extends true
|
|
80
|
+
? Parameters<typeof mount<Props<C>, Exports<C>>>[1]
|
|
81
|
+
: LegacyConstructorOptions<Props<C>>
|
|
82
|
+
|
|
83
|
+
/** A component's props or some of its mount options. */
|
|
84
|
+
export type ComponentOptions<C extends Component> =
|
|
85
|
+
| Props<C>
|
|
86
|
+
| Partial<MountOptions<C>>
|
|
87
|
+
|
|
88
|
+
/** Update a component's props and trigger updates to the DOM. */
|
|
89
|
+
export type Rerender<C extends Component> = (
|
|
90
|
+
props: Partial<Props<C>>
|
|
91
|
+
) => Promise<void>
|
|
92
|
+
|
|
93
|
+
/** The result of mounting a component into the document. */
|
|
94
|
+
export interface MountResult<C extends Component> {
|
|
95
|
+
/** The mounted component's exports. */
|
|
96
|
+
component: Exports<C>
|
|
97
|
+
/** Unmount the component. */
|
|
98
|
+
unmount: () => void
|
|
99
|
+
/** Rerender the component. */
|
|
100
|
+
rerender: Rerender<C>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Options for configuring the document. */
|
|
104
|
+
export interface SetupOptions {
|
|
105
|
+
/** The base document element, `document.body` if unspecified. */
|
|
106
|
+
baseElement?: HTMLElement
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The result of setting up the document for rendering. */
|
|
110
|
+
export interface SetupResult<C extends Component> {
|
|
111
|
+
/** The base document element, usually `document.body`. */
|
|
112
|
+
baseElement: HTMLElement
|
|
113
|
+
/** The component's immediate container element, usually a `<div>` appended to `document.body`. */
|
|
114
|
+
container: HTMLElement
|
|
115
|
+
/** Options to pass to `mount`. */
|
|
116
|
+
mountOptions: MountOptions<C>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The result of setting up the document and rendering the component. */
|
|
120
|
+
export interface RenderResult<C extends Component> {
|
|
121
|
+
/** The base document element, usually `document.body`. */
|
|
122
|
+
baseElement: HTMLElement
|
|
123
|
+
/** The component's immediate container element, usually a `<div>` appended to `document.body`. */
|
|
124
|
+
container: HTMLElement
|
|
125
|
+
/** The mounted component's exports. */
|
|
126
|
+
component: Exports<C>
|
|
127
|
+
/** Unmount the component. */
|
|
128
|
+
unmount: () => void
|
|
129
|
+
/** Rerender the component. */
|
|
130
|
+
rerender: Rerender<C>
|
|
131
|
+
}
|