@wippy-fe/webcomponent-core 0.0.25 → 0.0.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wippy-fe/webcomponent-core",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "Framework-agnostic base class for Wippy web components — shadow DOM, CSS loading, schema-driven prop parsing, and lifecycle management.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -11,7 +11,8 @@
11
11
  ],
12
12
  "peerDependencies": {
13
13
  "@iconify/vue": "^5.0.0",
14
- "@wippy-fe/proxy": "0.0.25"
14
+ "@wippy-fe/proxy": "0.0.27",
15
+ "@wippy-fe/shared": "0.0.27"
15
16
  },
16
17
  "license": "UNLICENSED"
17
18
  }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ *
4
+ * Covers `WippyElement.host` getter — reads `__wippyHost` attached by
5
+ * the managed-layout shell's content resolvers. And the cleanup path
6
+ * in `disconnectedCallback` that drops stale wires on detach.
7
+ */
8
+ import { describe, expect, it } from 'vitest'
9
+ import { WippyElement } from './base-element.ts'
10
+
11
+ // `ElementInternals.states` shim lives in `vitest.setup.ts` (gen-2-chat
12
+ // repo root) so any test that transitively imports `WippyElement` gets
13
+ // the patch. This file just exercises the resulting behavior.
14
+
15
+ class TestEl extends WippyElement {
16
+ static get wippyConfig() {
17
+ return { propsSchema: { properties: {} }, hostCssKeys: [] }
18
+ }
19
+
20
+ protected onMount(): void {}
21
+ protected onUnmount(): void {}
22
+ }
23
+
24
+ const TAG = `wippy-host-test-${Math.random().toString(36).slice(2)}`
25
+ customElements.define(TAG, TestEl)
26
+
27
+ describe('wippyElement.host getter', () => {
28
+ it('returns null on a fresh element with no wires', () => {
29
+ const el = document.createElement(TAG) as TestEl
30
+ expect(el.host).toBeNull()
31
+ })
32
+
33
+ it('returns the wrapper attached by the resolver as __wippyHost', () => {
34
+ const el = document.createElement(TAG) as TestEl
35
+ const fakeWrapper = { layout: { broadcast: () => {} } }
36
+ ;(el as unknown as { __wippyHost: unknown }).__wippyHost = fakeWrapper
37
+ expect(el.host).toBe(fakeWrapper)
38
+ })
39
+
40
+ it('connectedCallback can read this.host on first tick', () => {
41
+ let observedHost: unknown = 'sentinel'
42
+ class Probe extends WippyElement {
43
+ static get wippyConfig() {
44
+ return { propsSchema: { properties: {} }, hostCssKeys: [] }
45
+ }
46
+
47
+ protected onMount(): void {
48
+ observedHost = this.host
49
+ }
50
+
51
+ protected onUnmount(): void {}
52
+ }
53
+ const probeTag = `wippy-probe-${Math.random().toString(36).slice(2)}`
54
+ customElements.define(probeTag, Probe)
55
+ const el = document.createElement(probeTag)
56
+ const fakeWrapper = { layout: {} }
57
+ ;(el as unknown as { __wippyHost: unknown }).__wippyHost = fakeWrapper
58
+ document.body.appendChild(el)
59
+ expect(observedHost).toBe(fakeWrapper)
60
+ el.remove()
61
+ })
62
+ })
63
+
64
+ describe('wippyElement.disconnectedCallback wire cleanup', () => {
65
+ it('drops __wippyHost and __wippyHostBus on disconnect', () => {
66
+ const el = document.createElement(TAG) as TestEl
67
+ ;(el as unknown as { __wippyHost: unknown }).__wippyHost = { layout: {} }
68
+ ;(el as unknown as { __wippyHostBus: unknown }).__wippyHostBus = {
69
+ broadcast: () => {},
70
+ send: () => {},
71
+ subscribe: () => () => {},
72
+ }
73
+ document.body.appendChild(el)
74
+ el.remove()
75
+ expect((el as unknown as { __wippyHost?: unknown }).__wippyHost).toBeUndefined()
76
+ expect((el as unknown as { __wippyHostBus?: unknown }).__wippyHostBus).toBeUndefined()
77
+ })
78
+ })
79
+
80
+ describe('wippyElement reconnect — no shadow-child accumulation', () => {
81
+ // R3 regression: previously `connectedCallback` re-ran the full mount
82
+ // path (CSS injection + container append) on every reconnect. The
83
+ // `disconnectedCallback` did NOT clear the persistent shadow root, so
84
+ // each detach+reattach cycle leaked another `<style>` and another
85
+ // container `<div>`. On a move-style DOM op (e.g. `appendChild` of an
86
+ // already-connected element, list reorder) the browser fires
87
+ // disconnect → connect synchronously, so this leak fires on every
88
+ // reorder.
89
+ //
90
+ // Fix: gate CSS / container / icon setup on a `_initialized` flag and
91
+ // reuse the stored container on reconnect.
92
+
93
+ class CssEl extends WippyElement {
94
+ static get wippyConfig() {
95
+ return {
96
+ propsSchema: { properties: {} },
97
+ hostCssKeys: [],
98
+ inlineCss: ':host { display: block; }',
99
+ containerClasses: ['ct'],
100
+ }
101
+ }
102
+
103
+ public mountCount = 0
104
+ public lastIsRemount: boolean | undefined = undefined
105
+ public lastContainer: HTMLElement | null = null
106
+
107
+ protected onMount(
108
+ _shadow: ShadowRoot,
109
+ container: HTMLElement,
110
+ _props: Record<string, unknown>,
111
+ _errors: string[],
112
+ _content?: string | null,
113
+ isRemount?: boolean,
114
+ ): void {
115
+ this.mountCount += 1
116
+ this.lastIsRemount = isRemount
117
+ this.lastContainer = container
118
+ }
119
+
120
+ protected onUnmount(): void {}
121
+ }
122
+ const cssTag = `wippy-reconnect-${Math.random().toString(36).slice(2)}`
123
+ customElements.define(cssTag, CssEl)
124
+
125
+ it('does not duplicate <style> or container <div> across detach/reattach cycles', () => {
126
+ const el = document.createElement(cssTag) as CssEl
127
+ document.body.appendChild(el)
128
+
129
+ const shadow = el.shadowRoot!
130
+ const initialChildCount = shadow.childElementCount
131
+ const initialStyles = shadow.querySelectorAll('style').length
132
+ const initialContainers = shadow.querySelectorAll('div.ct').length
133
+ expect(initialContainers).toBe(1)
134
+ expect(initialStyles).toBeGreaterThan(0)
135
+ expect(el.mountCount).toBe(1)
136
+ expect(el.lastIsRemount).toBe(false)
137
+
138
+ // Detach + reattach (one cycle) — pre-fix this would add another
139
+ // <style> and another container <div>.
140
+ el.remove()
141
+ document.body.appendChild(el)
142
+
143
+ expect(shadow.childElementCount).toBe(initialChildCount)
144
+ expect(shadow.querySelectorAll('style').length).toBe(initialStyles)
145
+ expect(shadow.querySelectorAll('div.ct').length).toBe(initialContainers)
146
+ expect(el.mountCount).toBe(2)
147
+ expect(el.lastIsRemount).toBe(true)
148
+
149
+ // Second detach + reattach cycle — counts still stable.
150
+ el.remove()
151
+ document.body.appendChild(el)
152
+
153
+ expect(shadow.childElementCount).toBe(initialChildCount)
154
+ expect(shadow.querySelectorAll('style').length).toBe(initialStyles)
155
+ expect(shadow.querySelectorAll('div.ct').length).toBe(initialContainers)
156
+ expect(el.mountCount).toBe(3)
157
+ expect(el.lastIsRemount).toBe(true)
158
+
159
+ el.remove()
160
+ })
161
+
162
+ it('reuses the same container instance on reconnect', () => {
163
+ const el = document.createElement(cssTag) as CssEl
164
+ document.body.appendChild(el)
165
+ const firstContainer = el.lastContainer
166
+ expect(firstContainer).not.toBeNull()
167
+
168
+ el.remove()
169
+ document.body.appendChild(el)
170
+
171
+ expect(el.lastContainer).toBe(firstContainer)
172
+ el.remove()
173
+ })
174
+ })
@@ -1,8 +1,9 @@
1
- import { addIcons, define } from '@wippy-fe/proxy'
1
+ import type { WippyElementConfig } from './types.ts'
2
2
  import { addCollection } from '@iconify/vue'
3
- import { loadHostCss, injectInlineCss } from './css-loader.ts'
3
+ import { addIcons, define } from '@wippy-fe/proxy'
4
+ import { injectInlineCss, loadHostCss } from './css-loader.ts'
5
+ import { getWippyHost } from './host-scope.ts'
4
6
  import { parseProps } from './prop-parser.ts'
5
- import type { WippyElementConfig } from './types.ts'
6
7
 
7
8
  /**
8
9
  * Abstract base class for Wippy web components.
@@ -10,7 +11,7 @@ import type { WippyElementConfig } from './types.ts'
10
11
  * Generic over `Props` — the parsed prop object type. This types `onMount`,
11
12
  * `onPropsChanged`, and `validateProps`. Defaults to `Record<string, unknown>`.
12
13
  *
13
- * Lifecycle order:
14
+ * Lifecycle order (first connect):
14
15
  * 1. shadow DOM attached
15
16
  * 2. `onInit(shadow)` — hook for early shadow DOM customization
16
17
  * 3. inline CSS injected
@@ -23,12 +24,25 @@ import type { WippyElementConfig } from './types.ts'
23
24
  * 10. `onReady()` — post-mount hook
24
25
  * 11. `load` event emitted
25
26
  *
27
+ * On reconnect (move-style DOM op: detach → reattach), the shadow root
28
+ * persists across the cycle, so steps 1, 3, 4, 5 (CSS injection +
29
+ * container creation) are skipped to avoid duplicating `<style>` and
30
+ * container `<div>` siblings inside the persistent shadow. The stored
31
+ * container is reused and `onMount` is re-invoked with `isRemount = true`
32
+ * so framework adapters can decide between fresh-mount and remount paths.
33
+ *
26
34
  * On error: `onError(error)` → internals state → error → `error` event
27
- * On disconnect: `onUnmount()` → `unload` event → internals cleared
35
+ * On disconnect: `onUnmount()` → `unload` event → internals cleared.
36
+ * Note: `disconnectedCallback` does NOT clear the shadow root or the
37
+ * stored container — they're reused on reconnect.
28
38
  */
29
39
  export abstract class WippyElement<Props = Record<string, unknown>> extends HTMLElement {
30
40
  private _internals!: ElementInternals
31
41
  private _contentObserver: MutationObserver | null = null
42
+ /** True after the first successful connectedCallback completes. */
43
+ private _initialized: boolean = false
44
+ /** Container div created on first connect; reused on subsequent connects. */
45
+ private _container: HTMLElement | null = null
32
46
 
33
47
  /**
34
48
  * Override to provide the component's configuration.
@@ -58,6 +72,24 @@ export abstract class WippyElement<Props = Record<string, unknown>> extends HTML
58
72
  this._internals = this.attachInternals()
59
73
  }
60
74
 
75
+ /**
76
+ * Panel-scoped `host` wrapper attached by the managed-layout shell's
77
+ * content resolvers (`ComponentResolver` / `WebComponentPackageLoader`).
78
+ *
79
+ * Inside a managed-layout panel, this is a wrapper around the universal
80
+ * `host` API where context-aware calls (`layout.broadcast / send / on`)
81
+ * are routed through the panel-bound bus — so `sourcePanelId` is
82
+ * attributed correctly without postMessage indirection. Layout
83
+ * mutations and other host methods pass through unchanged.
84
+ *
85
+ * `null` outside a managed-layout context (compat shell, chat sidebar,
86
+ * standalone playground). Subclass code that needs a host in those
87
+ * cases can fall back to `import { host } from '@wippy-fe/proxy'`.
88
+ */
89
+ get host(): ReturnType<typeof getWippyHost> {
90
+ return getWippyHost(this)
91
+ }
92
+
61
93
  /**
62
94
  * Emit a CustomEvent that bubbles and crosses shadow DOM boundaries.
63
95
  */
@@ -75,33 +107,46 @@ export abstract class WippyElement<Props = Record<string, unknown>> extends HTML
75
107
  this._internals.states.add('loading')
76
108
  try {
77
109
  const config = (this.constructor as typeof WippyElement).wippyConfig
110
+ const isRemount = this._initialized
78
111
 
79
112
  // 1. Shadow DOM (guard against reconnect — attachShadow throws if called twice)
80
113
  const shadow = this.shadowRoot ?? this.attachShadow({ mode: config.shadowMode ?? 'open' })
81
114
 
82
- // 2. Early hook — customize shadow before CSS/container
83
- this.onInit(shadow)
115
+ let container: HTMLElement
116
+ if (!isRemount) {
117
+ // 2. Early hook — customize shadow before CSS/container
118
+ this.onInit(shadow)
84
119
 
85
- // 3. Inline CSS
86
- if (config.inlineCss) {
87
- injectInlineCss(shadow, config.inlineCss)
88
- }
120
+ // 3. Inline CSS
121
+ if (config.inlineCss) {
122
+ injectInlineCss(shadow, config.inlineCss)
123
+ }
89
124
 
90
- // 4. Host CSS (async, non-blocking)
91
- if (config.hostCssKeys === undefined || config.hostCssKeys.length > 0) {
92
- loadHostCss(shadow, config.hostCssKeys)
93
- }
125
+ // 4. Host CSS (async, non-blocking)
126
+ if (config.hostCssKeys === undefined || config.hostCssKeys.length > 0) {
127
+ loadHostCss(shadow, config.hostCssKeys)
128
+ }
94
129
 
95
- // 5. Container
96
- const container = document.createElement('div')
97
- const classes = config.containerClasses ?? []
98
- if (classes.length > 0) {
99
- container.classList.add(...classes)
100
- }
101
- shadow.appendChild(container)
130
+ // 5. Container
131
+ container = document.createElement('div')
132
+ const classes = config.containerClasses ?? []
133
+ if (classes.length > 0) {
134
+ container.classList.add(...classes)
135
+ }
136
+ shadow.appendChild(container)
137
+ this._container = container
102
138
 
103
- // 6. Icons
104
- addIcons(addCollection)
139
+ // 6. Icons
140
+ addIcons(addCollection)
141
+ }
142
+ else {
143
+ // Reconnect path — reuse the existing shadow children + container.
144
+ // Skip CSS/icon/container setup so we don't accumulate duplicate
145
+ // `<style>` and container `<div>` siblings on every detach/reattach.
146
+ // The framework adapter decides whether to remount on the existing
147
+ // container or recreate its app instance.
148
+ container = this._container!
149
+ }
105
150
 
106
151
  // 7. Parse initial props + custom validation
107
152
  const { props, errors } = parseProps(this, config.propsSchema)
@@ -125,19 +170,25 @@ export abstract class WippyElement<Props = Record<string, unknown>> extends HTML
125
170
  })
126
171
  }
127
172
 
128
- // 8. Framework mount
129
- this.onMount(shadow, container, typedProps, errors, initialContent)
173
+ // 8. Framework mount (first time) or remount (reconnect)
174
+ this.onMount(shadow, container, typedProps, errors, initialContent, isRemount)
130
175
 
131
176
  // 9. Ready
132
177
  this._internals.states.delete('loading')
133
178
  this._internals.states.add('ready')
134
179
 
180
+ // First-connect bookkeeping after a clean mount.
181
+ if (!isRemount) {
182
+ this._initialized = true
183
+ }
184
+
135
185
  // 10. Post-mount hook
136
186
  this.onReady()
137
187
 
138
188
  // 11. Emit load
139
189
  this.emitEvent('load')
140
- } catch (error) {
190
+ }
191
+ catch (error) {
141
192
  this.onError(error)
142
193
  this._internals.states.delete('loading')
143
194
  this._internals.states.add('error')
@@ -156,10 +207,17 @@ export abstract class WippyElement<Props = Record<string, unknown>> extends HTML
156
207
  this.onUnmount()
157
208
  this.emitEvent('unload')
158
209
  this._internals.states.clear()
210
+ // Drop panel-context wires so a detached element held by a
211
+ // consumer (or re-inserted into a different panel) doesn't carry
212
+ // stale wires that would silently misattribute. The resolver
213
+ // re-installs them on the next mount.
214
+ delete (this as unknown as { __wippyHost?: unknown }).__wippyHost
215
+ delete (this as unknown as { __wippyHostBus?: unknown }).__wippyHostBus
159
216
  }
160
217
 
161
218
  attributeChangedCallback(_name: string, oldVal: string | null, newVal: string | null): void {
162
- if (oldVal === newVal) return
219
+ if (oldVal === newVal)
220
+ return
163
221
  const config = (this.constructor as typeof WippyElement).wippyConfig
164
222
  const { props, errors } = parseProps(this, config.propsSchema)
165
223
  if (config.validateProps) {
@@ -173,13 +231,23 @@ export abstract class WippyElement<Props = Record<string, unknown>> extends HTML
173
231
  /** Called right after shadow DOM is attached, before CSS or container. */
174
232
  protected onInit(_shadow: ShadowRoot): void {}
175
233
 
176
- /** Called once after shadow DOM, CSS, and container are ready. Mount your framework here. */
234
+ /**
235
+ * Called after shadow DOM, CSS, and container are ready. Mount your
236
+ * framework here.
237
+ *
238
+ * Invoked on first connect AND on every subsequent reconnect (move-style
239
+ * DOM op: detach → reattach). On reconnect the same `container` instance
240
+ * is reused. Adapters that hold a framework app handle should either
241
+ * detect `isRemount === true` and re-mount on the existing container, or
242
+ * leave the previous app intact across the detach/reattach cycle.
243
+ */
177
244
  protected abstract onMount(
178
245
  shadow: ShadowRoot,
179
246
  container: HTMLElement,
180
247
  initialProps: Props,
181
248
  initialErrors: string[],
182
249
  initialContent?: string | null,
250
+ isRemount?: boolean,
183
251
  ): void
184
252
 
185
253
  /** Called after internals state is set to ready, before the `load` event. */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Layout bus types shared between `@wippy-fe/webcomponent-core` and
3
+ * `@wippy-fe/webcomponent-vue`. Mirror of the host-side
4
+ * `LayoutBusBound` / `BroadcastEnvelope` shapes; structurally
5
+ * compatible so a host-mounted web component can read its
6
+ * `__wippyHostBus` property without importing host-private types.
7
+ *
8
+ * The host (gen-2-chat managed-layout shell) sets:
9
+ * - `el.dataset.wippyPanelId` (string)
10
+ * - `(el as { __wippyHostBus?: LayoutBusBound }).__wippyHostBus`
11
+ * BEFORE the element is appended to the DOM. Web components see them
12
+ * on first `connectedCallback` tick.
13
+ *
14
+ * Outside a managed-layout host, both are absent — read code MUST
15
+ * tolerate `undefined`.
16
+ *
17
+ * The interface bodies live in `@wippy-fe/shared` so the host can
18
+ * import them without dragging in this package's `@wippy-fe/proxy`
19
+ * runtime dependency. Consumers continue to import from
20
+ * `@wippy-fe/webcomponent-core` (this re-export) for backward compat.
21
+ */
22
+
23
+ import type { LayoutBusBound } from '@wippy-fe/shared'
24
+
25
+ export type { BroadcastEnvelope, LayoutBusBound } from '@wippy-fe/shared'
26
+
27
+ /**
28
+ * Read the host bus attached to an element by `ComponentResolver` in
29
+ * the managed-layout host. Returns `null` outside a managed-layout
30
+ * host (or before the element is connected). Stable across element
31
+ * remounts of the same panel — the resolver re-attaches a fresh bus
32
+ * each time.
33
+ */
34
+ export function getWippyHostBus(el: HTMLElement): LayoutBusBound | null {
35
+ return (el as { __wippyHostBus?: LayoutBusBound }).__wippyHostBus ?? null
36
+ }
37
+
38
+ /**
39
+ * Read the panelId attribute set by `ComponentResolver`. `null`
40
+ * outside a managed-layout host.
41
+ */
42
+ export function getWippyPanelId(el: HTMLElement): string | null {
43
+ return el.dataset.wippyPanelId ?? null
44
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { describe, expect, it } from 'vitest'
5
+ import { getWippyHost } from './host-scope.ts'
6
+
7
+ describe('getWippyHost', () => {
8
+ it('returns the wrapper attached as __wippyHost', () => {
9
+ const el = document.createElement('div')
10
+ const fake = { layout: { broadcast: () => {} } }
11
+ ;(el as unknown as { __wippyHost: unknown }).__wippyHost = fake
12
+ expect(getWippyHost(el)).toBe(fake)
13
+ })
14
+
15
+ it('returns null when no wrapper attached', () => {
16
+ const el = document.createElement('div')
17
+ expect(getWippyHost(el)).toBeNull()
18
+ })
19
+ })
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Element-attached host wrapper reader for web components mounted as
3
+ * `kind: 'component'` panels in the gen-2-chat managed-layout shell.
4
+ *
5
+ * The host stamps a panel-scoped wrapper as `__wippyHost` on each
6
+ * element BEFORE it is appended to the DOM (see the host's
7
+ * `panel-context-wires.ts` and `host-scope.ts`). Consumer web
8
+ * components read it back via `getWippyHost(this)` in
9
+ * `connectedCallback` and onwards.
10
+ *
11
+ * The wrapper is **constructed by the host** — this package only
12
+ * exposes the reader. Outside a managed-layout host (raw mounts,
13
+ * compat shells, tests without the connector) the property is absent
14
+ * and `getWippyHost` returns `null`.
15
+ */
16
+
17
+ /**
18
+ * Read the panel-scoped host wrapper attached to an element by the
19
+ * managed-layout shell's content resolvers. Returns `null` outside a
20
+ * managed-layout host (or before the element is connected).
21
+ *
22
+ * Subclass authors of `WippyElement` / `WippyVueElement` access this
23
+ * via `this.host` / `useHost()` and rarely need to call this directly.
24
+ * The helper is exposed for raw `HTMLElement` consumers.
25
+ */
26
+ export function getWippyHost<H = unknown>(el: HTMLElement): H | null {
27
+ return ((el as unknown) as { __wippyHost?: H }).__wippyHost ?? null
28
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,13 @@
1
- export { WippyElement, define } from './base-element.ts'
2
- export { loadHostCss, injectInlineCss } from './css-loader.ts'
3
- export { parseProps, attrToCamel } from './prop-parser.ts'
1
+ export { define, WippyElement } from './base-element.ts'
2
+ export { injectInlineCss, loadHostCss } from './css-loader.ts'
3
+ export { getWippyHostBus, getWippyPanelId } from './host-bus.ts'
4
+ export type { BroadcastEnvelope, LayoutBusBound } from './host-bus.ts'
5
+ export { getWippyHost } from './host-scope.ts'
6
+ export { attrToCamel, parseProps } from './prop-parser.ts'
7
+ export type { ParseResult } from './prop-parser.ts'
4
8
  export type {
9
+ HostCssKey,
5
10
  WippyElementConfig,
6
- WippyPropsSchema,
7
11
  WippyPropDefinition,
8
- HostCssKey,
12
+ WippyPropsSchema,
9
13
  } from './types.ts'
10
- export type { ParseResult } from './prop-parser.ts'
@@ -1,4 +1,4 @@
1
- import type { WippyPropsSchema, WippyPropDefinition } from './types.ts'
1
+ import type { WippyPropDefinition, WippyPropsSchema } from './types.ts'
2
2
 
3
3
  export interface ParseResult {
4
4
  props: Record<string, unknown>
@@ -16,20 +16,22 @@ export function attrToCamel(attr: string): string {
16
16
  /**
17
17
  * Parses a single attribute value according to its schema definition.
18
18
  */
19
- function parseValue(attr: string, raw: string, def: WippyPropDefinition): { value: unknown; error?: string } {
19
+ function parseValue(attr: string, raw: string, def: WippyPropDefinition): { value: unknown, error?: string } {
20
20
  switch (def.type) {
21
21
  case 'string':
22
22
  return { value: raw }
23
23
 
24
24
  case 'number': {
25
- const n = parseFloat(raw)
26
- if (isNaN(n)) return { value: undefined, error: `Invalid ${attr}: expected a number` }
25
+ const n = Number.parseFloat(raw)
26
+ if (Number.isNaN(n))
27
+ return { value: undefined, error: `Invalid ${attr}: expected a number` }
27
28
  return { value: n }
28
29
  }
29
30
 
30
31
  case 'integer': {
31
- const n = parseInt(raw, 10)
32
- if (isNaN(n)) return { value: undefined, error: `Invalid ${attr}: expected an integer` }
32
+ const n = Number.parseInt(raw, 10)
33
+ if (Number.isNaN(n))
34
+ return { value: undefined, error: `Invalid ${attr}: expected an integer` }
33
35
  return { value: n }
34
36
  }
35
37
 
@@ -45,7 +47,8 @@ function parseValue(attr: string, raw: string, def: WippyPropDefinition): { valu
45
47
  return { value: undefined, error: `Invalid ${attr}: expected a JSON array` }
46
48
  }
47
49
  return { value: parsed }
48
- } catch {
50
+ }
51
+ catch {
49
52
  return { value: undefined, error: `Invalid ${attr}: must be valid JSON` }
50
53
  }
51
54
  }
@@ -78,7 +81,8 @@ export function parseProps(element: HTMLElement, schema: WippyPropsSchema): Pars
78
81
  const result = parseValue(attr, raw, def)
79
82
  if (result.error) {
80
83
  errors.push(result.error)
81
- } else {
84
+ }
85
+ else {
82
86
  props[camel] = result.value
83
87
  }
84
88
  }