Skip to content

Commit 93d4035

Browse files
authored
Merge branch 'main' into allow-hmr-plugins
2 parents bc02a16 + 9326e8d commit 93d4035

14 files changed

Lines changed: 5758 additions & 3623 deletions

package-lock.json

Lines changed: 5450 additions & 3394 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,34 @@
2727
"build:docs": "cd docs && JEKYLL_ENV=production bundle exec jekyll build",
2828
"clean": "tsc --build --clean",
2929
"lint": "eslint . --ignore-path .gitignore",
30-
"pretest": "npm run build",
31-
"test": "npm run lint && karma start test/karma.config.cjs",
32-
"postpublish": "npm publish --ignore-scripts --@github:registry='https://npm.pkg.github.com'"
30+
"postpublish": "npm publish --ignore-scripts --@github:registry='https://npm.pkg.github.com'",
31+
"presize": "npm run build",
32+
"size": "size-limit",
33+
"pretest": "npm run size && npm run lint",
34+
"test": "web-test-runner test/* --node-resolve"
3335
},
3436
"prettier": "@github/prettier-config",
3537
"devDependencies": {
3638
"@github/prettier-config": "^0.0.4",
3739
"@lhci/cli": "^0.7.0",
40+
"@open-wc/testing": "^3.1.2",
41+
"@size-limit/preset-small-lib": "^7.0.8",
3842
"@typescript-eslint/eslint-plugin": "^5.16.0",
3943
"@typescript-eslint/parser": "^5.16.0",
40-
"chai": "^4.3.0",
41-
"chai-spies": "^1.0.0",
42-
"chromium": "^3.0.3",
44+
"@web/dev-server-esbuild": "^0.3.0",
45+
"@web/test-runner": "^0.13.27",
4346
"eslint": "^8.12.0",
4447
"eslint-plugin-github": "^4.3.6",
45-
"karma": "^6.1.1",
46-
"karma-chai": "^0.1.0",
47-
"karma-chai-spies": "^0.1.4",
48-
"karma-chrome-launcher": "^3.1.0",
49-
"karma-mocha": "^2.0.1",
50-
"karma-mocha-reporter": "^2.2.5",
51-
"mocha": "^8.3.0",
48+
"sinon": "^13.0.1",
49+
"size-limit": "^7.0.8",
5250
"tslib": "^2.3.1",
5351
"typescript": "^4.6.3"
54-
}
52+
},
53+
"size-limit": [
54+
{
55+
"path": "lib/index.js",
56+
"import": "{controller, attr, target, targets}",
57+
"limit": "1.6kb"
58+
}
59+
]
5560
}

src/attr.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {CustomElement} from './custom-element.js'
2+
import {meta} from './core.js'
23

3-
const attrs = new WeakMap<Record<PropertyKey, unknown>, string[]>()
4+
const attrKey = 'attr'
45
type attrValue = string | number | boolean
56

67
/**
@@ -11,8 +12,7 @@ type attrValue = string | number | boolean
1112
* Number or Boolean. This matches the behavior of `initializeAttrs`.
1213
*/
1314
export function attr<K extends string>(proto: Record<K, attrValue>, key: K): void {
14-
if (!attrs.has(proto)) attrs.set(proto, [])
15-
attrs.get(proto)!.push(key)
15+
meta(proto, attrKey).add(key)
1616
}
1717

1818
/**
@@ -38,7 +38,7 @@ const initialized = new WeakSet<Element>()
3838
export function initializeAttrs(instance: HTMLElement, names?: Iterable<string>): void {
3939
if (initialized.has(instance)) return
4040
initialized.add(instance)
41-
if (!names) names = getAttrNames(Object.getPrototypeOf(instance))
41+
if (!names) names = meta(Object.getPrototypeOf(instance), attrKey)
4242
for (const key of names) {
4343
const value = (<Record<PropertyKey, unknown>>(<unknown>instance))[key]
4444
const name = attrToAttributeName(key)
@@ -79,19 +79,6 @@ export function initializeAttrs(instance: HTMLElement, names?: Iterable<string>)
7979
}
8080
}
8181

82-
function getAttrNames(classObjectProto: Record<PropertyKey, unknown>): Set<string> {
83-
const names: Set<string> = new Set()
84-
let proto: Record<PropertyKey, unknown> | typeof HTMLElement = classObjectProto
85-
86-
while (proto && proto !== HTMLElement) {
87-
const attrNames = attrs.get(<Record<PropertyKey, unknown>>proto) || []
88-
for (const name of attrNames) names.add(name)
89-
proto = Object.getPrototypeOf(proto)
90-
}
91-
92-
return names
93-
}
94-
9582
function attrToAttributeName(name: string): string {
9683
return `data-${name.replace(/([A-Z]($|[a-z]))/g, '-$1')}`.replace(/--/g, '-').toLowerCase()
9784
}
@@ -101,8 +88,7 @@ export function defineObservedAttributes(classObject: CustomElement): void {
10188
Object.defineProperty(classObject, 'observedAttributes', {
10289
configurable: true,
10390
get() {
104-
const attrMap = getAttrNames(classObject.prototype)
105-
return [...attrMap].map(attrToAttributeName).concat(observed)
91+
return [...meta(classObject.prototype, attrKey)].map(attrToAttributeName).concat(observed)
10692
},
10793
set(attributes: string[]) {
10894
observed = attributes

src/controller.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {initializeInstance, initializeClass, initializeAttributeChanged} from './core.js'
1+
import {CatalystDelegate} from './core.js'
22
import type {CustomElement} from './custom-element.js'
33
/**
44
* Controller is a decorator to be used over a class that extends HTMLElement.
@@ -7,18 +7,5 @@ import type {CustomElement} from './custom-element.js'
77
* wrapping the classes `connectedCallback` method if needed.
88
*/
99
export function controller(classObject: CustomElement): void {
10-
const connect = classObject.prototype.connectedCallback
11-
classObject.prototype.connectedCallback = function (this: HTMLElement) {
12-
initializeInstance(this, connect)
13-
}
14-
const attributeChanged = classObject.prototype.attributeChangedCallback
15-
classObject.prototype.attributeChangedCallback = function (
16-
this: HTMLElement,
17-
name: string,
18-
oldValue: unknown,
19-
newValue: unknown
20-
) {
21-
initializeAttributeChanged(this, name, oldValue, newValue, attributeChanged)
22-
}
23-
initializeClass(classObject)
10+
new CatalystDelegate(classObject)
2411
}

src/core.ts

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,91 @@ import {autoShadowRoot} from './auto-shadow-root.js'
44
import {defineObservedAttributes, initializeAttrs} from './attr.js'
55
import type {CustomElement} from './custom-element.js'
66

7-
const instances = new WeakSet<Element>()
8-
9-
export function initializeInstance(instance: HTMLElement, connect?: (this: HTMLElement) => void): void {
10-
instance.toggleAttribute('data-catalyst', true)
11-
customElements.upgrade(instance)
12-
instances.add(instance)
13-
autoShadowRoot(instance)
14-
initializeAttrs(instance)
15-
bind(instance)
16-
if (connect) connect.call(instance)
17-
if (instance.shadowRoot) bindShadow(instance.shadowRoot)
18-
}
7+
const symbol = Symbol.for('catalyst')
8+
9+
export class CatalystDelegate {
10+
constructor(classObject: CustomElement) {
11+
// eslint-disable-next-line @typescript-eslint/no-this-alias
12+
const delegate = this
13+
14+
const connectedCallback = classObject.prototype.connectedCallback
15+
classObject.prototype.connectedCallback = function (this: HTMLElement) {
16+
delegate.connectedCallback(this, connectedCallback)
17+
}
18+
19+
const disconnectedCallback = classObject.prototype.disconnectedCallback
20+
classObject.prototype.disconnectedCallback = function (this: HTMLElement) {
21+
delegate.disconnectedCallback(this, disconnectedCallback)
22+
}
1923

20-
export function initializeAttributeChanged(
21-
instance: HTMLElement,
22-
name: string,
23-
oldValue: unknown,
24-
newValue: unknown,
25-
attributeChangedCallback?: (this: HTMLElement, name: string, oldValue: unknown, newValue: unknown) => void
26-
): void {
27-
initializeAttrs(instance)
28-
if (name !== 'data-catalyst' && attributeChangedCallback) {
29-
attributeChangedCallback.call(instance, name, oldValue, newValue)
24+
const attributeChangedCallback = classObject.prototype.attributeChangedCallback
25+
classObject.prototype.attributeChangedCallback = function (
26+
this: HTMLElement,
27+
name: string,
28+
oldValue: string | null,
29+
newValue: string | null
30+
) {
31+
delegate.attributeChangedCallback(this, name, oldValue, newValue, attributeChangedCallback)
32+
}
33+
34+
let observedAttributes = classObject.observedAttributes || []
35+
Object.defineProperty(classObject, 'observedAttributes', {
36+
configurable: true,
37+
get() {
38+
return delegate.observedAttributes(this, observedAttributes)
39+
},
40+
set(attributes: string[]) {
41+
observedAttributes = attributes
42+
}
43+
})
44+
45+
defineObservedAttributes(classObject)
46+
register(classObject)
3047
}
31-
}
3248

33-
export function initializeClass(classObject: CustomElement): void {
34-
defineObservedAttributes(classObject)
35-
register(classObject)
49+
observedAttributes(instance: HTMLElement, observedAttributes: string[]) {
50+
return observedAttributes
51+
}
52+
53+
connectedCallback(instance: HTMLElement, connectedCallback: () => void) {
54+
instance.toggleAttribute('data-catalyst', true)
55+
customElements.upgrade(instance)
56+
autoShadowRoot(instance)
57+
initializeAttrs(instance)
58+
bind(instance)
59+
connectedCallback?.call(instance)
60+
if (instance.shadowRoot) bindShadow(instance.shadowRoot)
61+
}
62+
63+
disconnectedCallback(element: HTMLElement, disconnectedCallback: () => void) {
64+
disconnectedCallback?.call(element)
65+
}
66+
67+
attributeChangedCallback(
68+
instance: HTMLElement,
69+
name: string,
70+
oldValue: string | null,
71+
newValue: string | null,
72+
attributeChangedCallback: (...args: unknown[]) => void
73+
) {
74+
initializeAttrs(instance)
75+
if (name !== 'data-catalyst' && attributeChangedCallback) {
76+
attributeChangedCallback.call(instance, name, oldValue, newValue)
77+
}
78+
}
3679
}
3780

38-
export function initialized(el: Element): boolean {
39-
return instances.has(el)
81+
export function meta(proto: Record<PropertyKey, unknown>, name: string): Set<string> {
82+
if (!Object.prototype.hasOwnProperty.call(proto, symbol)) {
83+
const parent = proto[symbol] as Map<string, Set<string>> | undefined
84+
const map = (proto[symbol] = new Map<string, Set<string>>())
85+
if (parent) {
86+
for (const [key, value] of parent) {
87+
map.set(key, new Set(value))
88+
}
89+
}
90+
}
91+
const map = proto[symbol] as Map<string, Set<string>>
92+
if (!map.has(name)) map.set(name, new Set<string>())
93+
return map.get(name)!
4094
}

src/target.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {findTarget, findTargets} from './findtarget.js'
2+
import {meta} from './core.js'
23

34
/**
45
* Target is a decorator which - when assigned to a property field on the
@@ -8,6 +9,7 @@ import {findTarget, findTargets} from './findtarget.js'
89
* `findTarget(this, 'foo')`.
910
*/
1011
export function target<K extends string>(proto: Record<K, unknown>, key: K): void {
12+
meta(proto, 'target').add(key)
1113
Object.defineProperty(proto, key, {
1214
configurable: true,
1315
get() {
@@ -24,6 +26,7 @@ export function target<K extends string>(proto: Record<K, unknown>, key: K): voi
2426
* `findTargets(this, 'foo')`.
2527
*/
2628
export function targets<K extends string>(proto: Record<K, unknown>, key: K): void {
29+
meta(proto, 'targets').add(key)
2730
Object.defineProperty(proto, key, {
2831
configurable: true,
2932
get() {

test/attr.js renamed to test/attr.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {expect} from '@open-wc/testing'
12
import {initializeAttrs, defineObservedAttributes, attr} from '../lib/attr.js'
23

34
describe('initializeAttrs', () => {
@@ -154,13 +155,15 @@ describe('initializeAttrs', () => {
154155
})
155156

156157
describe('attr', () => {
157-
class AttrTestElement extends HTMLElement {}
158-
attr(AttrTestElement.prototype, 'foo')
159-
attr(AttrTestElement.prototype, 'bar')
158+
class AttrTestElement extends HTMLElement {
159+
@attr foo
160+
@attr bar
161+
}
160162
window.customElements.define('attr-test-element', AttrTestElement)
161163

162-
class ExtendedAttrTestElement extends AttrTestElement {}
163-
attr(ExtendedAttrTestElement.prototype, 'baz')
164+
class ExtendedAttrTestElement extends AttrTestElement {
165+
@attr baz
166+
}
164167
window.customElements.define('extended-attr-test-element', ExtendedAttrTestElement)
165168

166169
it('populates the "default" list for initializeAttrs', () => {
@@ -182,7 +185,7 @@ describe('attr', () => {
182185
expect(instance).to.have.property('foo', '')
183186
expect(instance).to.have.property('bar', 'hello')
184187
expect(instance).to.have.property('baz', 'world')
185-
expect(instance.getAttributeNames()).to.eql(['data-baz', 'data-foo', 'data-bar'])
188+
expect(instance.getAttributeNames()).to.eql(['data-foo', 'data-bar', 'data-baz'])
186189
expect(instance.getAttribute('data-foo')).to.equal('')
187190
expect(instance.getAttribute('data-bar')).to.equal('hello')
188191
expect(instance.getAttribute('data-baz')).to.equal('world')
@@ -213,25 +216,28 @@ describe('defineObservedAttributes', () => {
213216
})
214217

215218
it('will reflect values from attr calls', () => {
216-
class TestElement extends HTMLElement {}
219+
class TestElement extends HTMLElement {
220+
@attr foo
221+
}
217222
defineObservedAttributes(TestElement)
218-
attr(TestElement.prototype, 'foo')
219223
expect(TestElement.observedAttributes).to.eql(['data-foo'])
220224
})
221225

222226
it('will reflect values even if set after definition', () => {
223-
class TestElement extends HTMLElement {}
227+
class TestElement extends HTMLElement {
228+
@attr foo
229+
}
224230
defineObservedAttributes(TestElement)
225-
attr(TestElement.prototype, 'foo')
226231
TestElement.observedAttributes = ['a', 'b', 'c']
227232
expect(TestElement.observedAttributes).to.eql(['data-foo', 'a', 'b', 'c'])
228233
})
229234

230235
it('will reflect values from extended elements', () => {
231-
class TestElement extends HTMLElement {}
236+
class TestElement extends HTMLElement {
237+
@attr foo
238+
}
232239
class ExtendedTestElement extends TestElement {}
233240
defineObservedAttributes(ExtendedTestElement)
234-
attr(TestElement.prototype, 'foo')
235241
expect(ExtendedTestElement.observedAttributes).to.eql(['data-foo'])
236242
})
237243
})
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {expect} from '@open-wc/testing'
2+
import {replace, fake} from 'sinon'
13
import {autoShadowRoot} from '../lib/auto-shadow-root.js'
24

35
describe('autoShadowRoot', () => {
@@ -74,15 +76,19 @@ describe('autoShadowRoot', () => {
7476
instance.appendChild(template)
7577

7678
let shadowRoot = null
77-
chai.spy.on(instance, 'attachShadow', (...args) => {
78-
shadowRoot = Element.prototype.attachShadow.apply(instance, args)
79-
return shadowRoot
80-
})
79+
replace(
80+
instance,
81+
'attachShadow',
82+
fake((...args) => {
83+
shadowRoot = Element.prototype.attachShadow.apply(instance, args)
84+
return shadowRoot
85+
})
86+
)
8187

8288
autoShadowRoot(instance)
8389

8490
expect(instance).to.have.property('shadowRoot').equal(null)
85-
expect(instance.attachShadow).to.have.been.called.once.with.exactly({mode: 'closed'})
91+
expect(instance.attachShadow).to.have.been.calledOnceWith({mode: 'closed'})
8692
expect(shadowRoot.textContent).to.equal('Hello World')
8793
})
8894
})

0 commit comments

Comments
 (0)