@operato/shell 10.11.0 → 10.15.0
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/CHANGELOG.md +22 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/require-custom-element.ts.html +742 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/test/fixtures/guarded-define.ts.html +118 -0
- package/coverage/lcov-report/test/fixtures/index.html +161 -0
- package/coverage/lcov-report/test/fixtures/no-registration.ts.html +91 -0
- package/coverage/lcov-report/test/fixtures/registers.ts.html +118 -0
- package/coverage/lcov-report/test/fixtures/unguarded-define.ts.html +112 -0
- package/coverage/lcov.info +395 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/require-custom-element.d.ts +111 -0
- package/dist/src/require-custom-element.js +184 -0
- package/dist/src/require-custom-element.js.map +1 -0
- package/dist/test/fixtures/guarded-define.d.ts +8 -0
- package/dist/test/fixtures/guarded-define.js +12 -0
- package/dist/test/fixtures/guarded-define.js.map +1 -0
- package/dist/test/fixtures/no-registration.d.ts +3 -0
- package/dist/test/fixtures/no-registration.js +4 -0
- package/dist/test/fixtures/no-registration.js.map +1 -0
- package/dist/test/fixtures/registers.d.ts +10 -0
- package/dist/test/fixtures/registers.js +12 -0
- package/dist/test/fixtures/registers.js.map +1 -0
- package/dist/test/fixtures/unguarded-define.d.ts +8 -0
- package/dist/test/fixtures/unguarded-define.js +10 -0
- package/dist/test/fixtures/unguarded-define.js.map +1 -0
- package/dist/test/require-custom-element.test.d.ts +1 -0
- package/dist/test/require-custom-element.test.js +269 -0
- package/dist/test/require-custom-element.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/fixtures/guarded-define.ts +11 -0
- package/test/fixtures/no-registration.ts +2 -0
- package/test/fixtures/registers.ts +11 -0
- package/test/fixtures/unguarded-define.ts +9 -0
- package/test/require-custom-element.test.ts +287 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@operato/shell",
|
|
3
3
|
"description": "WebApplication architecturing shell following open-wc recommendations",
|
|
4
4
|
"author": "heartyoh",
|
|
5
|
-
"version": "10.
|
|
5
|
+
"version": "10.15.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/src/index.js",
|
|
8
8
|
"module": "dist/src/index.js",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@material/web": "^2.0.0",
|
|
63
|
-
"@operato/graphql": "^10.
|
|
64
|
-
"@operato/popup": "^10.
|
|
65
|
-
"@operato/styles": "^10.
|
|
66
|
-
"@operato/utils": "^10.
|
|
63
|
+
"@operato/graphql": "^10.15.0",
|
|
64
|
+
"@operato/popup": "^10.15.0",
|
|
65
|
+
"@operato/styles": "^10.15.0",
|
|
66
|
+
"@operato/utils": "^10.15.0",
|
|
67
67
|
"@webcomponents/scoped-custom-element-registry": "^0.0.9",
|
|
68
68
|
"lit": "^3.1.2",
|
|
69
69
|
"lodash-es": "^4.17.21",
|
|
@@ -102,5 +102,5 @@
|
|
|
102
102
|
"prettier --write"
|
|
103
103
|
]
|
|
104
104
|
},
|
|
105
|
-
"gitHead": "
|
|
105
|
+
"gitHead": "62ac2a23866e7ea579786c4433ef356888dfded8"
|
|
106
106
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hand-written component that checks the registry before defining.
|
|
3
|
+
*
|
|
4
|
+
* Nothing throws, the module resolves, and somebody else's constructor stays registered. Only the
|
|
5
|
+
* identity comparison sees this.
|
|
6
|
+
*/
|
|
7
|
+
export class Guarded extends HTMLElement {}
|
|
8
|
+
|
|
9
|
+
if (!customElements.get('probe-guarded-clash')) {
|
|
10
|
+
customElements.define('probe-guarded-clash', Guarded)
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A module that registers its element while being evaluated, the way lit's decorator does.
|
|
3
|
+
*
|
|
4
|
+
* A real module rather than a thunk that throws, because *when* the define runs is the whole
|
|
5
|
+
* point: it runs during evaluation, so a clash takes the module down with it and the caller sees a
|
|
6
|
+
* rejected import rather than a bad return value. A synthetic throw would skip the module loader
|
|
7
|
+
* and prove nothing about that.
|
|
8
|
+
*/
|
|
9
|
+
export class Registers extends HTMLElement {}
|
|
10
|
+
|
|
11
|
+
customElements.define('probe-registers', Registers)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The losing half of a clash: defines unconditionally, exactly as `@customElement` compiles to.
|
|
3
|
+
*
|
|
4
|
+
* The test registers this tag first, so evaluating this module throws NotSupportedError out of the
|
|
5
|
+
* loader — the shape a real duplicate arrives in.
|
|
6
|
+
*/
|
|
7
|
+
export class Unguarded extends HTMLElement {}
|
|
8
|
+
|
|
9
|
+
customElements.define('probe-unguarded-clash', Unguarded)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing'
|
|
2
|
+
|
|
3
|
+
import { forgetRegisteredElements, requireCustomElement } from '../src/require-custom-element.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every case here is a planted defect.
|
|
7
|
+
*
|
|
8
|
+
* A guard that has never been made to fail is indistinguishable from a guard that is dead: both
|
|
9
|
+
* report nothing. So each branch gets a situation built to trip it, and the assertion is that the
|
|
10
|
+
* guard names *that* branch — not merely that it complained.
|
|
11
|
+
*
|
|
12
|
+
* The two clash cases matter most. A clash is the accident this repo has actually had, it is the
|
|
13
|
+
* rarest branch to reach, and it arrives by two different routes that a single test would leave
|
|
14
|
+
* half covered.
|
|
15
|
+
*
|
|
16
|
+
* ── Why the fixtures are real modules ───────────────────────────────────────
|
|
17
|
+
* The failing paths import files under `fixtures/` rather than calling a thunk that throws. A
|
|
18
|
+
* module's define runs while the module is being evaluated, and that is exactly what decides
|
|
19
|
+
* whether the caller sees a rejection or a bad return value; a synthetic throw skips the loader and
|
|
20
|
+
* would agree with the guard for the wrong reason.
|
|
21
|
+
*
|
|
22
|
+
* These tests run on the platform's own loader. Webpack's is a second question, answered by
|
|
23
|
+
* reading its shipped runtime rather than here — the header of the module under test carries that
|
|
24
|
+
* measurement, including why a retried clash comes back as an empty module.
|
|
25
|
+
*
|
|
26
|
+
* Fixture tags are fixed strings and a module evaluates once per page, so each fixture belongs to
|
|
27
|
+
* exactly one test.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Unique per test, so registrations never leak between them — the registry cannot be undone. */
|
|
31
|
+
let counter = 0
|
|
32
|
+
function freshTag(): string {
|
|
33
|
+
counter += 1
|
|
34
|
+
return `require-probe-${counter}-${Math.random().toString(36).slice(2, 8)}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('requireCustomElement', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
/* The table of who registered what is page-wide; tests must not inherit each other's winners. */
|
|
40
|
+
forgetRegisteredElements()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('passes when the specifier registers the element and the registered class is that one', async () => {
|
|
44
|
+
const check = await requireCustomElement({
|
|
45
|
+
name: 'probe-registers',
|
|
46
|
+
load: () => import('./fixtures/registers.js') as unknown as Promise<Record<string, unknown>>,
|
|
47
|
+
specifier: './fixtures/registers.js',
|
|
48
|
+
exportName: 'Registers'
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(check.ok).to.be.true
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('reports load-failed when the import rejects', async () => {
|
|
55
|
+
/*
|
|
56
|
+
* Held in a variable so the compiler leaves it alone: the point is a specifier that resolves at
|
|
57
|
+
* build time and fails in the browser, which is the shape this repo's import mistakes take.
|
|
58
|
+
*/
|
|
59
|
+
const missing = './fixtures/not-a-file.js'
|
|
60
|
+
|
|
61
|
+
const check = await requireCustomElement({
|
|
62
|
+
name: freshTag(),
|
|
63
|
+
load: () => import(missing) as unknown as Promise<Record<string, unknown>>,
|
|
64
|
+
specifier: missing,
|
|
65
|
+
exportName: 'Element'
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(check.ok).to.be.false
|
|
69
|
+
if (check.ok) return
|
|
70
|
+
expect(check.reason).to.equal('load-failed')
|
|
71
|
+
/* The specifier is the thing to change, so it has to appear in the line that says so. */
|
|
72
|
+
expect(check.detail).to.contain('./fixtures/not-a-file.js')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('reports missing-from-module when the import resolves without registering', async () => {
|
|
76
|
+
const check = await requireCustomElement({
|
|
77
|
+
name: freshTag(),
|
|
78
|
+
load: () => import('./fixtures/no-registration.js') as unknown as Promise<Record<string, unknown>>,
|
|
79
|
+
specifier: './fixtures/no-registration.js',
|
|
80
|
+
exportName: 'NeverRegistered'
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(check.ok).to.be.false
|
|
84
|
+
if (check.ok) return
|
|
85
|
+
expect(check.reason).to.equal('missing-from-module')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('reports duplicate when an unguarded define clashes and takes its module down', async () => {
|
|
89
|
+
/* Another copy got there first. */
|
|
90
|
+
class Theirs extends HTMLElement {}
|
|
91
|
+
customElements.define('probe-unguarded-clash', Theirs)
|
|
92
|
+
|
|
93
|
+
const check = await requireCustomElement({
|
|
94
|
+
name: 'probe-unguarded-clash',
|
|
95
|
+
load: () => import('./fixtures/unguarded-define.js') as unknown as Promise<Record<string, unknown>>,
|
|
96
|
+
specifier: './fixtures/unguarded-define.js',
|
|
97
|
+
exportName: 'Unguarded'
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
expect(check.ok).to.be.false
|
|
101
|
+
if (check.ok) return
|
|
102
|
+
/*
|
|
103
|
+
* The import rejects here, so without the NotSupportedError branch this reads as a wrong path —
|
|
104
|
+
* and the fix for a wrong path (change the specifier) is the opposite of the fix for this
|
|
105
|
+
* (delete one of the two copies).
|
|
106
|
+
*/
|
|
107
|
+
expect(check.reason).to.equal('duplicate')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('reports duplicate when the module guards its own define and stays quiet', async () => {
|
|
111
|
+
class Theirs extends HTMLElement {}
|
|
112
|
+
customElements.define('probe-guarded-clash', Theirs)
|
|
113
|
+
|
|
114
|
+
const check = await requireCustomElement({
|
|
115
|
+
name: 'probe-guarded-clash',
|
|
116
|
+
load: () => import('./fixtures/guarded-define.js') as unknown as Promise<Record<string, unknown>>,
|
|
117
|
+
specifier: './fixtures/guarded-define.js',
|
|
118
|
+
exportName: 'Guarded'
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(check.ok).to.be.false
|
|
122
|
+
if (check.ok) return
|
|
123
|
+
/* Nothing threw and the registry answers truthy, so only the constructor identity sees this. */
|
|
124
|
+
expect(check.reason).to.equal('duplicate')
|
|
125
|
+
expect(customElements.get('probe-guarded-clash')).to.equal(Theirs)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('names both specifiers in a clash once one of them has passed a check', async () => {
|
|
129
|
+
const tag = freshTag()
|
|
130
|
+
|
|
131
|
+
class Mine extends HTMLElement {}
|
|
132
|
+
const winner = '@scope/pkg/dist-client/index.js'
|
|
133
|
+
const loser = '@scope/pkg/client'
|
|
134
|
+
|
|
135
|
+
await requireCustomElement({
|
|
136
|
+
name: tag,
|
|
137
|
+
load: async () => {
|
|
138
|
+
customElements.define(tag, Mine)
|
|
139
|
+
return { Element: Mine }
|
|
140
|
+
},
|
|
141
|
+
specifier: winner,
|
|
142
|
+
exportName: 'Element'
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const check = await requireCustomElement({
|
|
146
|
+
name: tag,
|
|
147
|
+
load: async () => ({ Element: class extends HTMLElement {} }),
|
|
148
|
+
specifier: loser,
|
|
149
|
+
exportName: 'Element'
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(check.ok).to.be.false
|
|
153
|
+
if (check.ok) return
|
|
154
|
+
/*
|
|
155
|
+
* A clash is the one failure whose fix lives in a file the complaining host does not name.
|
|
156
|
+
* Without the winner the reader starts at grep; with it, the line is the whole answer.
|
|
157
|
+
*/
|
|
158
|
+
expect(check.detail).to.contain(winner)
|
|
159
|
+
expect(check.detail).to.contain(loser)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('calls a retried clash a clash, not a wrong export name', async () => {
|
|
163
|
+
const tag = freshTag()
|
|
164
|
+
|
|
165
|
+
class Mine extends HTMLElement {}
|
|
166
|
+
await requireCustomElement({
|
|
167
|
+
name: tag,
|
|
168
|
+
load: async () => {
|
|
169
|
+
customElements.define(tag, Mine)
|
|
170
|
+
return { Element: Mine }
|
|
171
|
+
},
|
|
172
|
+
specifier: '@scope/pkg/dist-client/index.js',
|
|
173
|
+
exportName: 'Element'
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* Models what webpack does, measured off its shipped runtime: a module that threw stays cached
|
|
178
|
+
* with empty exports and no error recorded, so importing it a second time resolves with `{}`.
|
|
179
|
+
* The tag is registered by someone, and the export is missing — which is also what a caller
|
|
180
|
+
* with the wrong exportName looks like. Only the record of who registered it separates them,
|
|
181
|
+
* and sending this reader to fix an exportName would be sending them to the wrong file.
|
|
182
|
+
*/
|
|
183
|
+
const check = await requireCustomElement({
|
|
184
|
+
name: tag,
|
|
185
|
+
load: async () => ({}),
|
|
186
|
+
specifier: '@scope/pkg/client',
|
|
187
|
+
exportName: 'Element'
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
expect(check.ok).to.be.false
|
|
191
|
+
if (check.ok) return
|
|
192
|
+
expect(check.reason).to.equal('duplicate')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('reports export-not-found when the caller names the wrong export and no one else owns the tag', async () => {
|
|
196
|
+
const tag = freshTag()
|
|
197
|
+
|
|
198
|
+
const check = await requireCustomElement({
|
|
199
|
+
name: tag,
|
|
200
|
+
load: async () => {
|
|
201
|
+
customElements.define(tag, class extends HTMLElement {})
|
|
202
|
+
return { Element: class extends HTMLElement {} }
|
|
203
|
+
},
|
|
204
|
+
specifier: '@scope/pkg/dist-client/index.js',
|
|
205
|
+
exportName: 'WrongName'
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect(check.ok).to.be.false
|
|
209
|
+
if (check.ok) return
|
|
210
|
+
/*
|
|
211
|
+
* The element registered correctly here. Comparing against an absent export would have made a
|
|
212
|
+
* healthy load look like a clash and sent someone hunting for a second copy that is not there.
|
|
213
|
+
*/
|
|
214
|
+
expect(check.reason).to.equal('export-not-found')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('falls back to no-answer only when the import never settles', async () => {
|
|
218
|
+
const check = await requireCustomElement({
|
|
219
|
+
name: freshTag(),
|
|
220
|
+
load: () => new Promise<Record<string, unknown>>(() => {}),
|
|
221
|
+
specifier: '@scope/pkg/dist-client/index.js',
|
|
222
|
+
exportName: 'Element',
|
|
223
|
+
timeout: 50
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect(check.ok).to.be.false
|
|
227
|
+
if (check.ok) return
|
|
228
|
+
expect(check.reason).to.equal('no-answer')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('does not time out a slow import that does arrive', async () => {
|
|
232
|
+
const tag = freshTag()
|
|
233
|
+
|
|
234
|
+
const check = await requireCustomElement({
|
|
235
|
+
name: tag,
|
|
236
|
+
load: async () => {
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 60))
|
|
238
|
+
class Element extends HTMLElement {}
|
|
239
|
+
customElements.define(tag, Element)
|
|
240
|
+
return { Element }
|
|
241
|
+
},
|
|
242
|
+
specifier: '@scope/pkg/dist-client/index.js',
|
|
243
|
+
exportName: 'Element',
|
|
244
|
+
timeout: 300
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
* Slow is not wrong. The backstop exists for an import that never settles, and a load that is
|
|
249
|
+
* merely late must pass — otherwise the guard answers a question about the network with a
|
|
250
|
+
* sentence about the code.
|
|
251
|
+
*/
|
|
252
|
+
expect(check.ok).to.be.true
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('gives the viewer a different line for a clash than for a part that did not load', async () => {
|
|
256
|
+
const missingTag = freshTag()
|
|
257
|
+
const clashTag = freshTag()
|
|
258
|
+
|
|
259
|
+
class Theirs extends HTMLElement {}
|
|
260
|
+
customElements.define(clashTag, Theirs)
|
|
261
|
+
|
|
262
|
+
const missing = await requireCustomElement({
|
|
263
|
+
name: missingTag,
|
|
264
|
+
load: async () => ({ Element: class extends HTMLElement {} }),
|
|
265
|
+
specifier: '@scope/pkg/dist-client/index.js',
|
|
266
|
+
exportName: 'Element'
|
|
267
|
+
})
|
|
268
|
+
const clash = await requireCustomElement({
|
|
269
|
+
name: clashTag,
|
|
270
|
+
load: async () => ({ Element: class extends HTMLElement {} }),
|
|
271
|
+
specifier: '@scope/pkg/dist-client/index.js',
|
|
272
|
+
exportName: 'Element'
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
expect(missing.ok).to.be.false
|
|
276
|
+
expect(clash.ok).to.be.false
|
|
277
|
+
if (missing.ok || clash.ok) return
|
|
278
|
+
|
|
279
|
+
/*
|
|
280
|
+
* The wording lives in the guard so the three docks cannot drift apart, and the two situations
|
|
281
|
+
* have to reach the viewer as different sentences — one is a part that is absent, the other a
|
|
282
|
+
* part that is present twice.
|
|
283
|
+
*/
|
|
284
|
+
expect(missing.key).to.not.equal(clash.key)
|
|
285
|
+
expect(missing.defaultValue).to.not.equal(clash.defaultValue)
|
|
286
|
+
})
|
|
287
|
+
})
|