From 1e7ea223421b87f24b0470a3f4ce22a657d76f2d Mon Sep 17 00:00:00 2001 From: Buddy Date: Sat, 6 Jun 2026 14:28:51 -0700 Subject: [PATCH] fix: prevent tooltip crash from empty-string pane option buildOptions was passing default values for absent attributes, including empty-string defaults like pane:'', which caused Leaflet to look up this._panes[''] and return undefined, crashing in appendChild. - Skip empty-string defaults in buildOptions so Leaflet uses its own - Broaden createChildRegisterHandler param from LayerGroup to Layer so non-container layers (marker, circle, etc.) can intercept popup/tooltip children and call bindPopup/bindTooltip instead of bubbling to the map - Add child-registration handler to every layer component (all 12) - Fix disconnectedCallback in container components to unbind popups and tooltips and remove child layers properly --- src/components/leaflet-circle-marker.ts | 7 +++++++ src/components/leaflet-circle.ts | 7 +++++++ src/components/leaflet-feature-group.ts | 13 +++++++------ src/components/leaflet-geojson.ts | 9 ++++++--- src/components/leaflet-image-overlay.ts | 7 +++++++ src/components/leaflet-layer-group.ts | 16 +++++++--------- src/components/leaflet-marker.ts | 7 +++++++ src/components/leaflet-polygon.ts | 7 +++++++ src/components/leaflet-polyline.ts | 7 +++++++ src/components/leaflet-rectangle.ts | 7 +++++++ src/components/leaflet-svg-overlay.ts | 7 +++++++ src/components/leaflet-tile-layer-wms.ts | 7 +++++++ src/components/leaflet-tile-layer.ts | 7 +++++++ src/components/leaflet-video-overlay.ts | 7 +++++++ src/core/register.ts | 10 +++++----- src/core/utils.ts | 2 +- 16 files changed, 103 insertions(+), 24 deletions(-) diff --git a/src/components/leaflet-circle-marker.ts b/src/components/leaflet-circle-marker.ts index 32e91f6..e12bd45 100644 --- a/src/components/leaflet-circle-marker.ts +++ b/src/components/leaflet-circle-marker.ts @@ -1,5 +1,6 @@ import { CircleMarker } from 'leaflet'; import { registerWithParent, buildOptions } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; @@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletCircleMarker extends TypedBase { #obj?: CircleMarker; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -49,10 +52,14 @@ export class LeafletCircleMarker extends TypedBase { [this.#num('lat'), this.#num('lng')], buildOptions(this, PROPS, ['lat', 'lng']), ); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-circle.ts b/src/components/leaflet-circle.ts index 9264ee8..ecbf73f 100644 --- a/src/components/leaflet-circle.ts +++ b/src/components/leaflet-circle.ts @@ -1,5 +1,6 @@ import { Circle } from 'leaflet'; import { registerWithParent, buildOptions } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; @@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletCircle extends TypedBase { #obj?: Circle; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -49,10 +52,14 @@ export class LeafletCircle extends TypedBase { [this.#num('lat'), this.#num('lng')], buildOptions(this, PROPS, ['lat', 'lng']), ); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-feature-group.ts b/src/components/leaflet-feature-group.ts index 6a86e12..1a60136 100644 --- a/src/components/leaflet-feature-group.ts +++ b/src/components/leaflet-feature-group.ts @@ -1,9 +1,7 @@ -import { FeatureGroup } from 'leaflet'; +import { FeatureGroup, Layer } from 'leaflet'; import { buildOptions, registerWithParent } from '../core/utils.js'; import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; -const PROPS = {} as const satisfies Record; - export class LeafletFeatureGroup extends HTMLElement { #obj?: FeatureGroup; #children = new Map(); @@ -14,15 +12,18 @@ export class LeafletFeatureGroup extends HTMLElement { } connectedCallback() { - this.#obj = new FeatureGroup([], buildOptions(this, PROPS)); + this.#obj = new FeatureGroup([], buildOptions(this, {})); this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { - if (this.#childHandler) { - this.removeEventListener('leaflet-register', this.#childHandler); + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + for (const [el, entry] of this.#children) { + if (entry.type === 'popup') this.#obj?.unbindPopup(); + else if (entry.type === 'tooltip') this.#obj?.unbindTooltip(); + else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer); } this.#children.clear(); this.#obj?.remove(); diff --git a/src/components/leaflet-geojson.ts b/src/components/leaflet-geojson.ts index cb17491..b2a8c18 100644 --- a/src/components/leaflet-geojson.ts +++ b/src/components/leaflet-geojson.ts @@ -1,4 +1,4 @@ -import { GeoJSON, PathOptions } from 'leaflet'; +import { GeoJSON, PathOptions, Layer } from 'leaflet'; import { buildOptions, registerWithParent } from '../core/utils.js'; import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; @@ -66,8 +66,11 @@ export class LeafletGeoJSON extends TypedBase { } disconnectedCallback() { - if (this.#childHandler) { - this.removeEventListener('leaflet-register', this.#childHandler); + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + for (const [el, entry] of this.#children) { + if (entry.type === 'popup') this.#obj?.unbindPopup(); + else if (entry.type === 'tooltip') this.#obj?.unbindTooltip(); + else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer); } this.#children.clear(); this.#obj?.remove(); diff --git a/src/components/leaflet-image-overlay.ts b/src/components/leaflet-image-overlay.ts index a6bce84..b1e5ca8 100644 --- a/src/components/leaflet-image-overlay.ts +++ b/src/components/leaflet-image-overlay.ts @@ -1,5 +1,6 @@ import { ImageOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet'; import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; const PROPS = { @@ -23,6 +24,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletImageOverlay extends TypedBase { #obj?: ImageOverlay; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -54,10 +57,14 @@ export class LeafletImageOverlay extends TypedBase { this.#parsedBounds(), buildOptions(this, PROPS, ['url', 'bounds']), ); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-layer-group.ts b/src/components/leaflet-layer-group.ts index ff26102..05b5442 100644 --- a/src/components/leaflet-layer-group.ts +++ b/src/components/leaflet-layer-group.ts @@ -1,9 +1,7 @@ -import { LayerGroup } from 'leaflet'; +import { LayerGroup, Layer } from 'leaflet'; import { buildOptions, registerWithParent } from '../core/utils.js'; import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; -const PROPS = {} as const satisfies Record; - export class LeafletLayerGroup extends HTMLElement { #obj?: LayerGroup; #children = new Map(); @@ -14,18 +12,18 @@ export class LeafletLayerGroup extends HTMLElement { } connectedCallback() { - this.#obj = new LayerGroup([], buildOptions(this, PROPS)); + this.#obj = new LayerGroup([], buildOptions(this, {})); this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { - if (this.#childHandler) { - this.removeEventListener('leaflet-register', this.#childHandler); - } - for (const [el] of this.#children) { - this.#obj?.removeLayer(el as unknown as LayerGroup); + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + for (const [el, entry] of this.#children) { + if (entry.type === 'popup') this.#obj?.unbindPopup(); + else if (entry.type === 'tooltip') this.#obj?.unbindTooltip(); + else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer); } this.#children.clear(); this.#obj?.remove(); diff --git a/src/components/leaflet-marker.ts b/src/components/leaflet-marker.ts index 47085ea..635e804 100644 --- a/src/components/leaflet-marker.ts +++ b/src/components/leaflet-marker.ts @@ -1,5 +1,6 @@ import { Marker } from 'leaflet'; import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; const PROPS = { @@ -21,6 +22,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletMarker extends TypedBase { #obj?: Marker; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -50,10 +53,14 @@ export class LeafletMarker extends TypedBase { [this.#num('lat'), this.#num('lng')], buildOptions(this, PROPS, ['lat', 'lng']), ); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-polygon.ts b/src/components/leaflet-polygon.ts index 45810e7..119c394 100644 --- a/src/components/leaflet-polygon.ts +++ b/src/components/leaflet-polygon.ts @@ -1,5 +1,6 @@ import { Polygon } from 'leaflet'; import { registerWithParent, buildOptions } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; import type { LeafletLine } from './leaflet-line.js'; @@ -19,6 +20,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletPolygon extends TypedBase { #obj?: Polygon; #observer?: MutationObserver; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -45,6 +48,8 @@ export class LeafletPolygon extends TypedBase { connectedCallback() { this.#obj = new Polygon(this.#getCoords(), buildOptions(this, PROPS)); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); this.addEventListener('line-updated', this.#syncCoords); @@ -56,6 +61,8 @@ export class LeafletPolygon extends TypedBase { this.#observer?.disconnect(); this.#observer = undefined; this.removeEventListener('line-updated', this.#syncCoords); + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-polyline.ts b/src/components/leaflet-polyline.ts index 1283e18..d55d1fa 100644 --- a/src/components/leaflet-polyline.ts +++ b/src/components/leaflet-polyline.ts @@ -1,5 +1,6 @@ import { Polyline } from 'leaflet'; import { registerWithParent, buildOptions } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; import type { LeafletLine } from './leaflet-line.js'; @@ -19,6 +20,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletPolyline extends TypedBase { #obj?: Polyline; #observer?: MutationObserver; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -45,6 +48,8 @@ export class LeafletPolyline extends TypedBase { connectedCallback() { this.#obj = new Polyline(this.#getCoords(), buildOptions(this, PROPS)); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); this.addEventListener('line-updated', this.#syncCoords); @@ -56,6 +61,8 @@ export class LeafletPolyline extends TypedBase { this.#observer?.disconnect(); this.#observer = undefined; this.removeEventListener('line-updated', this.#syncCoords); + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-rectangle.ts b/src/components/leaflet-rectangle.ts index 3480790..673aa4e 100644 --- a/src/components/leaflet-rectangle.ts +++ b/src/components/leaflet-rectangle.ts @@ -1,5 +1,6 @@ import { Rectangle, LatLngBoundsExpression } from 'leaflet'; import { registerWithParent, buildOptions } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; @@ -18,6 +19,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletRectangle extends TypedBase { #obj?: Rectangle; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -44,10 +47,14 @@ export class LeafletRectangle extends TypedBase { connectedCallback() { this.#obj = new Rectangle(this.#parsedBounds(), buildOptions(this, PROPS, ['bounds'])); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-svg-overlay.ts b/src/components/leaflet-svg-overlay.ts index dff4d7e..02c788c 100644 --- a/src/components/leaflet-svg-overlay.ts +++ b/src/components/leaflet-svg-overlay.ts @@ -1,5 +1,6 @@ import { SVGOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet'; import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; const PROPS = { @@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletSVGOverlay extends TypedBase { #obj?: SVGOverlay; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -52,10 +55,14 @@ export class LeafletSVGOverlay extends TypedBase { this.#parsedBounds(), buildOptions(this, PROPS, ['bounds']), ); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-tile-layer-wms.ts b/src/components/leaflet-tile-layer-wms.ts index fa2e0ae..2cf1a67 100644 --- a/src/components/leaflet-tile-layer-wms.ts +++ b/src/components/leaflet-tile-layer-wms.ts @@ -1,5 +1,6 @@ import { TileLayer } from 'leaflet'; import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; const PROPS = { @@ -21,6 +22,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletTileLayerWMS extends TypedBase { #obj?: TileLayer.WMS; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -53,10 +56,14 @@ export class LeafletTileLayerWMS extends TypedBase { url, buildOptions(this, PROPS, ['url']) as Record, ); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-tile-layer.ts b/src/components/leaflet-tile-layer.ts index 8bfc936..f880a83 100644 --- a/src/components/leaflet-tile-layer.ts +++ b/src/components/leaflet-tile-layer.ts @@ -1,5 +1,6 @@ import { TileLayer } from 'leaflet'; import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; const PROPS = { @@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletTileLayer extends TypedBase { #obj?: TileLayer; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -45,10 +48,14 @@ export class LeafletTileLayer extends TypedBase { connectedCallback() { const url = this.getAttribute('url') || ''; this.#obj = new TileLayer(url, buildOptions(this, PROPS, ['url'])); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/components/leaflet-video-overlay.ts b/src/components/leaflet-video-overlay.ts index 014475c..1d5d8d3 100644 --- a/src/components/leaflet-video-overlay.ts +++ b/src/components/leaflet-video-overlay.ts @@ -1,5 +1,6 @@ import { VideoOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet'; import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js'; +import { createChildRegisterHandler, type ChildEntry } from '../core/register.js'; import type { PropDef, PropTypesFromTable } from '../types/props.js'; const PROPS = { @@ -24,6 +25,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletVideoOverlay extends TypedBase { #obj?: VideoOverlay; + #children = new Map(); + #childHandler?: EventListener; static get observedAttributes() { return Object.values(PROPS).map((s) => s.attr); @@ -55,10 +58,14 @@ export class LeafletVideoOverlay extends TypedBase { this.#parsedBounds(), buildOptions(this, PROPS, ['url', 'bounds']), ); + this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener; + this.addEventListener('leaflet-register', this.#childHandler); registerWithParent(this, this.#obj); } disconnectedCallback() { + if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler); + this.#children.clear(); this.#obj?.remove(); this.#obj = undefined; } diff --git a/src/core/register.ts b/src/core/register.ts index 28fa2c7..2c5a62c 100644 --- a/src/core/register.ts +++ b/src/core/register.ts @@ -12,7 +12,7 @@ export type ChildEntry = { }; export function createChildRegisterHandler( - container: LayerGroup, + layer: Layer, children: globalThis.Map, ) { return (e: LeafletRegisterEvent) => { @@ -20,15 +20,15 @@ export function createChildRegisterHandler( const el = e.detail.element; if (obj instanceof Popup) { e.stopPropagation(); - container.bindPopup(obj); + layer.bindPopup(obj); children.set(el, { type: 'popup' }); } else if (obj instanceof Tooltip) { e.stopPropagation(); - container.bindTooltip(obj); + layer.bindTooltip(obj); children.set(el, { type: 'tooltip' }); - } else if (obj instanceof Layer && 'addLayer' in container) { + } else if (obj instanceof Layer && 'addLayer' in layer) { e.stopPropagation(); - container.addLayer(obj); + (layer as unknown as LayerGroup).addLayer(obj); children.set(el, { type: 'layer' }); } }; diff --git a/src/core/utils.ts b/src/core/utils.ts index 8bac4e3..472c9d1 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -27,7 +27,7 @@ export function buildOptions( if (exclude.includes(propName)) continue; const val = el.getAttribute(spec.attr); if (val === null) { - if ('default' in spec) opts[propName] = spec.default; + if ('default' in spec && spec.default !== '') opts[propName] = spec.default; continue; } if (spec.kind === 'num') opts[propName] = Number(val);