@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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check that a custom element a screen depends on is actually there, and say why when it is not.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists ─────────────────────────────────────────────────────────
|
|
5
|
+
* The browser does not treat an unknown tag as an error. It draws an empty inline box and says
|
|
6
|
+
* nothing, so four different situations produce the same pixel:
|
|
7
|
+
*
|
|
8
|
+
* the element is still on its way
|
|
9
|
+
* the specifier does not bring that element at all
|
|
10
|
+
* two copies registered the same tag and the loser threw where nobody caught it
|
|
11
|
+
* this screen was never supposed to show one
|
|
12
|
+
*
|
|
13
|
+
* That default leans the wrong way: the quiet outcome is the broken one. This turns the first
|
|
14
|
+
* three into a stated reason, addressed to the person who can fix each.
|
|
15
|
+
*
|
|
16
|
+
* ── Why it does not wait on a clock ─────────────────────────────────────────
|
|
17
|
+
* A deadline would answer "slow" and "wrong" with the same words, and those are fixed in
|
|
18
|
+
* different places — one is bundle size, the other is one specifier. Given only a deadline, the
|
|
19
|
+
* next person raises the deadline, and the guard grows into a spinner that hides real slowness.
|
|
20
|
+
*
|
|
21
|
+
* The caller already holds a better signal: it knows when its own import settled. Every branch
|
|
22
|
+
* below is read from that, not from elapsed time. `timeout` is a backstop for a load that never
|
|
23
|
+
* settles at all, and is deliberately long — it must never be the thing that answers.
|
|
24
|
+
*
|
|
25
|
+
* ── Why the identity check, when a clash already throws ─────────────────────
|
|
26
|
+
* A second `customElements.define` of the same tag throws NotSupportedError during module
|
|
27
|
+
* evaluation, so the usual duplicate surfaces as a rejected import. Two paths dodge that:
|
|
28
|
+
*
|
|
29
|
+
* - a component that guards its own define (`if (!customElements.get(n))`) succeeds while
|
|
30
|
+
* somebody else's constructor stays registered
|
|
31
|
+
* - a barrel specifier, which is the common case here — the registration happens in a module
|
|
32
|
+
* the barrel re-exports, so the barrel itself has nothing to throw
|
|
33
|
+
*
|
|
34
|
+
* Both leave a registered element that is not the one this caller loaded. Comparing the
|
|
35
|
+
* constructor is the only thing that sees it: `customElements.get()` is truthy either way.
|
|
36
|
+
*
|
|
37
|
+
* ── What the bundler does with that error, measured ─────────────────────────
|
|
38
|
+
* Discriminating on `NotSupportedError` only works if the error reaches us unwrapped, and our
|
|
39
|
+
* imports go through webpack rather than the platform. Read off the shipped runtime, :3000,
|
|
40
|
+
* 2026-09-15:
|
|
41
|
+
*
|
|
42
|
+
* import() → Promise.all([__webpack_require__.e(chunk)…]).then(() => __webpack_require__(id))
|
|
43
|
+
* __webpack_require__ calls the module factory with **no try/catch** around it
|
|
44
|
+
*
|
|
45
|
+
* So a throw during module evaluation leaves `__webpack_require__` as itself and rejects that
|
|
46
|
+
* promise as itself. The discriminator holds.
|
|
47
|
+
*
|
|
48
|
+
* The same reading turns up a second thing, which is why `export-not-found` is not decided on its
|
|
49
|
+
* own: that runtime has no `cachedModule.error` and never deletes a failed entry, so the module
|
|
50
|
+
* that threw stays in the cache with `exports: {}`. **A second import of it resolves, empty.** A
|
|
51
|
+
* retry after a clash therefore arrives here looking like a caller who named the wrong export.
|
|
52
|
+
* The record of who registered the tag first is what tells those apart.
|
|
53
|
+
*
|
|
54
|
+
* @license Copyright © HatioLab Inc. All rights reserved.
|
|
55
|
+
*/
|
|
56
|
+
/** What the host knows and the guard cannot: the tag, where it comes from, and which export it is. */
|
|
57
|
+
export type RequiredElement = {
|
|
58
|
+
/** The tag name the host writes in its template. */
|
|
59
|
+
name: string;
|
|
60
|
+
/**
|
|
61
|
+
* The import that is supposed to bring that tag.
|
|
62
|
+
*
|
|
63
|
+
* A thunk rather than a specifier string, because only the host's own module graph can resolve
|
|
64
|
+
* it — resolving it here would make this module the importer and defeat the check.
|
|
65
|
+
*/
|
|
66
|
+
load: () => Promise<Record<string, unknown>>;
|
|
67
|
+
/**
|
|
68
|
+
* The specifier, verbatim, for the developer half of the message.
|
|
69
|
+
*
|
|
70
|
+
* The guard cannot read it off the thunk, and it is the single most useful thing to print: in
|
|
71
|
+
* this repo the usual defect is one specifier written two ways.
|
|
72
|
+
*/
|
|
73
|
+
specifier: string;
|
|
74
|
+
/** Which export of that module is the element's class. */
|
|
75
|
+
exportName: string;
|
|
76
|
+
/**
|
|
77
|
+
* Backstop for a load that never settles. Not a latency budget — see the header.
|
|
78
|
+
*
|
|
79
|
+
* @default 10000
|
|
80
|
+
*/
|
|
81
|
+
timeout?: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* What went wrong, in the words of the person who fixes it.
|
|
85
|
+
*
|
|
86
|
+
* Wider than the set of messages a viewer sees: `export-not-found` is a mistake in the call
|
|
87
|
+
* itself, and to a viewer it is indistinguishable from the element not being there.
|
|
88
|
+
*/
|
|
89
|
+
export type ElementFailure = 'duplicate' | 'missing-from-module' | 'export-not-found' | 'load-failed' | 'no-answer';
|
|
90
|
+
export type ElementCheck = {
|
|
91
|
+
ok: true;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
reason: ElementFailure;
|
|
95
|
+
/** i18n key for the viewer-facing line. Hosts translate; the wording is defined once, here. */
|
|
96
|
+
key: string;
|
|
97
|
+
/** Fallback text for that key, so a host with no translation still says something true. */
|
|
98
|
+
defaultValue: string;
|
|
99
|
+
/** The developer-facing line: what to change, and where. Never shown to a viewer. */
|
|
100
|
+
detail: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Resolve to `{ ok: true }` when `name` is registered and the registered constructor is the one
|
|
104
|
+
* `load()` brought; otherwise to the reason it is not.
|
|
105
|
+
*
|
|
106
|
+
* Never throws and never draws anything: the caller owns its own empty state, and only the caller
|
|
107
|
+
* knows whether a missing part means a disabled corner or a dead screen.
|
|
108
|
+
*/
|
|
109
|
+
export declare function requireCustomElement(required: RequiredElement): Promise<ElementCheck>;
|
|
110
|
+
/** Test seam. Never call this from a screen — the table is a fact about the page, not state to reset. */
|
|
111
|
+
export declare function forgetRegisteredElements(): void;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check that a custom element a screen depends on is actually there, and say why when it is not.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists ─────────────────────────────────────────────────────────
|
|
5
|
+
* The browser does not treat an unknown tag as an error. It draws an empty inline box and says
|
|
6
|
+
* nothing, so four different situations produce the same pixel:
|
|
7
|
+
*
|
|
8
|
+
* the element is still on its way
|
|
9
|
+
* the specifier does not bring that element at all
|
|
10
|
+
* two copies registered the same tag and the loser threw where nobody caught it
|
|
11
|
+
* this screen was never supposed to show one
|
|
12
|
+
*
|
|
13
|
+
* That default leans the wrong way: the quiet outcome is the broken one. This turns the first
|
|
14
|
+
* three into a stated reason, addressed to the person who can fix each.
|
|
15
|
+
*
|
|
16
|
+
* ── Why it does not wait on a clock ─────────────────────────────────────────
|
|
17
|
+
* A deadline would answer "slow" and "wrong" with the same words, and those are fixed in
|
|
18
|
+
* different places — one is bundle size, the other is one specifier. Given only a deadline, the
|
|
19
|
+
* next person raises the deadline, and the guard grows into a spinner that hides real slowness.
|
|
20
|
+
*
|
|
21
|
+
* The caller already holds a better signal: it knows when its own import settled. Every branch
|
|
22
|
+
* below is read from that, not from elapsed time. `timeout` is a backstop for a load that never
|
|
23
|
+
* settles at all, and is deliberately long — it must never be the thing that answers.
|
|
24
|
+
*
|
|
25
|
+
* ── Why the identity check, when a clash already throws ─────────────────────
|
|
26
|
+
* A second `customElements.define` of the same tag throws NotSupportedError during module
|
|
27
|
+
* evaluation, so the usual duplicate surfaces as a rejected import. Two paths dodge that:
|
|
28
|
+
*
|
|
29
|
+
* - a component that guards its own define (`if (!customElements.get(n))`) succeeds while
|
|
30
|
+
* somebody else's constructor stays registered
|
|
31
|
+
* - a barrel specifier, which is the common case here — the registration happens in a module
|
|
32
|
+
* the barrel re-exports, so the barrel itself has nothing to throw
|
|
33
|
+
*
|
|
34
|
+
* Both leave a registered element that is not the one this caller loaded. Comparing the
|
|
35
|
+
* constructor is the only thing that sees it: `customElements.get()` is truthy either way.
|
|
36
|
+
*
|
|
37
|
+
* ── What the bundler does with that error, measured ─────────────────────────
|
|
38
|
+
* Discriminating on `NotSupportedError` only works if the error reaches us unwrapped, and our
|
|
39
|
+
* imports go through webpack rather than the platform. Read off the shipped runtime, :3000,
|
|
40
|
+
* 2026-09-15:
|
|
41
|
+
*
|
|
42
|
+
* import() → Promise.all([__webpack_require__.e(chunk)…]).then(() => __webpack_require__(id))
|
|
43
|
+
* __webpack_require__ calls the module factory with **no try/catch** around it
|
|
44
|
+
*
|
|
45
|
+
* So a throw during module evaluation leaves `__webpack_require__` as itself and rejects that
|
|
46
|
+
* promise as itself. The discriminator holds.
|
|
47
|
+
*
|
|
48
|
+
* The same reading turns up a second thing, which is why `export-not-found` is not decided on its
|
|
49
|
+
* own: that runtime has no `cachedModule.error` and never deletes a failed entry, so the module
|
|
50
|
+
* that threw stays in the cache with `exports: {}`. **A second import of it resolves, empty.** A
|
|
51
|
+
* retry after a clash therefore arrives here looking like a caller who named the wrong export.
|
|
52
|
+
* The record of who registered the tag first is what tells those apart.
|
|
53
|
+
*
|
|
54
|
+
* @license Copyright © HatioLab Inc. All rights reserved.
|
|
55
|
+
*/
|
|
56
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
57
|
+
/** One marker object, so the backstop is told apart from a real rejection by identity, not by text. */
|
|
58
|
+
const TIMED_OUT = Symbol('require-custom-element: timed out');
|
|
59
|
+
/**
|
|
60
|
+
* Tag name → the specifier whose module was confirmed to have registered it.
|
|
61
|
+
*
|
|
62
|
+
* A clash is the one failure whose fix is in a file the complaining host does not name: you are
|
|
63
|
+
* told your import lost, and finding the winner is left to grep. This module is a singleton, every
|
|
64
|
+
* dock goes through it, and a passing check already knows both halves — so remember the winner and
|
|
65
|
+
* a later loser can be told who it collided with.
|
|
66
|
+
*
|
|
67
|
+
* Only successful checks write here, so the table never names a specifier that did not register.
|
|
68
|
+
*/
|
|
69
|
+
const registeredBy = new Map();
|
|
70
|
+
/**
|
|
71
|
+
* The viewer-facing wording, in one place.
|
|
72
|
+
*
|
|
73
|
+
* Two reasons share a key on purpose. A viewer can act on "this part did not load" and cannot act
|
|
74
|
+
* on which export name was wrong, so splitting that line would be detail addressed to nobody.
|
|
75
|
+
*/
|
|
76
|
+
const VIEWER_TEXT = {
|
|
77
|
+
duplicate: {
|
|
78
|
+
key: 'text.element-loaded-twice',
|
|
79
|
+
defaultValue: 'This part was loaded twice and could not start.'
|
|
80
|
+
},
|
|
81
|
+
'missing-from-module': {
|
|
82
|
+
key: 'text.element-did-not-load',
|
|
83
|
+
defaultValue: 'This part could not be loaded.'
|
|
84
|
+
},
|
|
85
|
+
'export-not-found': {
|
|
86
|
+
key: 'text.element-did-not-load',
|
|
87
|
+
defaultValue: 'This part could not be loaded.'
|
|
88
|
+
},
|
|
89
|
+
'load-failed': {
|
|
90
|
+
key: 'text.element-did-not-load',
|
|
91
|
+
defaultValue: 'This part could not be loaded.'
|
|
92
|
+
},
|
|
93
|
+
'no-answer': {
|
|
94
|
+
key: 'text.element-did-not-answer',
|
|
95
|
+
defaultValue: 'This part is taking too long to load.'
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function failure(reason, detail) {
|
|
99
|
+
return { ok: false, reason, ...VIEWER_TEXT[reason], detail };
|
|
100
|
+
}
|
|
101
|
+
/** Both halves of a clash when the winner is known, and an honest gap when it is not. */
|
|
102
|
+
function clashDetail(name, specifier, note) {
|
|
103
|
+
const winner = registeredBy.get(name);
|
|
104
|
+
return (`<${name}> was loaded from two places. ` +
|
|
105
|
+
(winner
|
|
106
|
+
? `first: ${winner} · second: ${specifier}. `
|
|
107
|
+
: `second: ${specifier}; the first got there before any check ran, so it is not named here. `) +
|
|
108
|
+
`Delete one of the two rather than renaming the tag — every consumer in this repo has to ` +
|
|
109
|
+
`import it by the same specifier. ${note}`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve to `{ ok: true }` when `name` is registered and the registered constructor is the one
|
|
113
|
+
* `load()` brought; otherwise to the reason it is not.
|
|
114
|
+
*
|
|
115
|
+
* Never throws and never draws anything: the caller owns its own empty state, and only the caller
|
|
116
|
+
* knows whether a missing part means a disabled corner or a dead screen.
|
|
117
|
+
*/
|
|
118
|
+
export async function requireCustomElement(required) {
|
|
119
|
+
const { name, load, specifier, exportName, timeout = DEFAULT_TIMEOUT } = required;
|
|
120
|
+
let timer;
|
|
121
|
+
let module;
|
|
122
|
+
try {
|
|
123
|
+
module = await Promise.race([
|
|
124
|
+
load(),
|
|
125
|
+
new Promise((_, reject) => {
|
|
126
|
+
timer = setTimeout(() => reject(TIMED_OUT), timeout);
|
|
127
|
+
})
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error === TIMED_OUT) {
|
|
132
|
+
return failure('no-answer', `importing ${specifier} for <${name}> did not settle within ${timeout}ms — the import never ` +
|
|
133
|
+
`resolved or rejected, so nothing can be said about the element yet`);
|
|
134
|
+
}
|
|
135
|
+
/*
|
|
136
|
+
* A clash throws here rather than further down: the decorator calls `customElements.define`
|
|
137
|
+
* while the module is being evaluated, so the losing copy takes its whole module with it.
|
|
138
|
+
*
|
|
139
|
+
* Match on the error's name, not its sentence — the wording is the engine's, not the spec's.
|
|
140
|
+
* The registry check is what makes the match safe: NotSupportedError only means "this tag is
|
|
141
|
+
* already taken" when the tag is, in fact, taken.
|
|
142
|
+
*/
|
|
143
|
+
if (error?.name === 'NotSupportedError' && customElements.get(name)) {
|
|
144
|
+
return failure('duplicate', clashDetail(name, specifier, `Its define threw where nobody caught it.`));
|
|
145
|
+
}
|
|
146
|
+
return failure('load-failed', `importing ${specifier} for <${name}> rejected: ${String(error?.message ?? error)}`);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
}
|
|
151
|
+
const registered = customElements.get(name);
|
|
152
|
+
const loaded = module[exportName];
|
|
153
|
+
if (!registered) {
|
|
154
|
+
return failure('missing-from-module', `${specifier} loaded but did not register <${name}>. That specifier does not bring this ` +
|
|
155
|
+
`element — check it against the one the package documents`);
|
|
156
|
+
}
|
|
157
|
+
if (loaded === undefined) {
|
|
158
|
+
/*
|
|
159
|
+
* The identity check cannot run, and two very different things land here.
|
|
160
|
+
*
|
|
161
|
+
* A caller who named the wrong export is one. The other is a retry of a module that already
|
|
162
|
+
* lost a clash: the bundler keeps the failed module cached with empty exports, so the second
|
|
163
|
+
* import resolves with nothing in it (see the header). The tag is registered by someone in both
|
|
164
|
+
* cases, so the registry cannot separate them — but the record of who registered it can.
|
|
165
|
+
*/
|
|
166
|
+
const winner = registeredBy.get(name);
|
|
167
|
+
if (winner !== undefined && winner !== specifier) {
|
|
168
|
+
return failure('duplicate', clashDetail(name, specifier, `Its module came back empty, which is what a module that already lost a clash returns.`));
|
|
169
|
+
}
|
|
170
|
+
return failure('export-not-found', `${specifier} has no export named "${exportName}", so <${name}> cannot be verified. ` +
|
|
171
|
+
`Fix the exportName passed to requireCustomElement`);
|
|
172
|
+
}
|
|
173
|
+
if (registered !== loaded) {
|
|
174
|
+
return failure('duplicate', clashDetail(name, specifier, `Nothing threw: this copy either guards its own define, or the specifier is a barrel whose ` +
|
|
175
|
+
`re-exported module never ran.`));
|
|
176
|
+
}
|
|
177
|
+
registeredBy.set(name, specifier);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
/** Test seam. Never call this from a screen — the table is a fact about the page, not state to reset. */
|
|
181
|
+
export function forgetRegisteredElements() {
|
|
182
|
+
registeredBy.clear();
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=require-custom-element.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-custom-element.js","sourceRoot":"","sources":["../../src/require-custom-element.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Check that a custom element a screen depends on is actually there, and say why when it is not.\n *\n * ── Why this exists ─────────────────────────────────────────────────────────\n * The browser does not treat an unknown tag as an error. It draws an empty inline box and says\n * nothing, so four different situations produce the same pixel:\n *\n * the element is still on its way\n * the specifier does not bring that element at all\n * two copies registered the same tag and the loser threw where nobody caught it\n * this screen was never supposed to show one\n *\n * That default leans the wrong way: the quiet outcome is the broken one. This turns the first\n * three into a stated reason, addressed to the person who can fix each.\n *\n * ── Why it does not wait on a clock ─────────────────────────────────────────\n * A deadline would answer \"slow\" and \"wrong\" with the same words, and those are fixed in\n * different places — one is bundle size, the other is one specifier. Given only a deadline, the\n * next person raises the deadline, and the guard grows into a spinner that hides real slowness.\n *\n * The caller already holds a better signal: it knows when its own import settled. Every branch\n * below is read from that, not from elapsed time. `timeout` is a backstop for a load that never\n * settles at all, and is deliberately long — it must never be the thing that answers.\n *\n * ── Why the identity check, when a clash already throws ─────────────────────\n * A second `customElements.define` of the same tag throws NotSupportedError during module\n * evaluation, so the usual duplicate surfaces as a rejected import. Two paths dodge that:\n *\n * - a component that guards its own define (`if (!customElements.get(n))`) succeeds while\n * somebody else's constructor stays registered\n * - a barrel specifier, which is the common case here — the registration happens in a module\n * the barrel re-exports, so the barrel itself has nothing to throw\n *\n * Both leave a registered element that is not the one this caller loaded. Comparing the\n * constructor is the only thing that sees it: `customElements.get()` is truthy either way.\n *\n * ── What the bundler does with that error, measured ─────────────────────────\n * Discriminating on `NotSupportedError` only works if the error reaches us unwrapped, and our\n * imports go through webpack rather than the platform. Read off the shipped runtime, :3000,\n * 2026-09-15:\n *\n * import() → Promise.all([__webpack_require__.e(chunk)…]).then(() => __webpack_require__(id))\n * __webpack_require__ calls the module factory with **no try/catch** around it\n *\n * So a throw during module evaluation leaves `__webpack_require__` as itself and rejects that\n * promise as itself. The discriminator holds.\n *\n * The same reading turns up a second thing, which is why `export-not-found` is not decided on its\n * own: that runtime has no `cachedModule.error` and never deletes a failed entry, so the module\n * that threw stays in the cache with `exports: {}`. **A second import of it resolves, empty.** A\n * retry after a clash therefore arrives here looking like a caller who named the wrong export.\n * The record of who registered the tag first is what tells those apart.\n *\n * @license Copyright © HatioLab Inc. All rights reserved.\n */\n\n/** What the host knows and the guard cannot: the tag, where it comes from, and which export it is. */\nexport type RequiredElement = {\n /** The tag name the host writes in its template. */\n name: string\n\n /**\n * The import that is supposed to bring that tag.\n *\n * A thunk rather than a specifier string, because only the host's own module graph can resolve\n * it — resolving it here would make this module the importer and defeat the check.\n */\n load: () => Promise<Record<string, unknown>>\n\n /**\n * The specifier, verbatim, for the developer half of the message.\n *\n * The guard cannot read it off the thunk, and it is the single most useful thing to print: in\n * this repo the usual defect is one specifier written two ways.\n */\n specifier: string\n\n /** Which export of that module is the element's class. */\n exportName: string\n\n /**\n * Backstop for a load that never settles. Not a latency budget — see the header.\n *\n * @default 10000\n */\n timeout?: number\n}\n\n/**\n * What went wrong, in the words of the person who fixes it.\n *\n * Wider than the set of messages a viewer sees: `export-not-found` is a mistake in the call\n * itself, and to a viewer it is indistinguishable from the element not being there.\n */\nexport type ElementFailure = 'duplicate' | 'missing-from-module' | 'export-not-found' | 'load-failed' | 'no-answer'\n\nexport type ElementCheck =\n | { ok: true }\n | {\n ok: false\n reason: ElementFailure\n /** i18n key for the viewer-facing line. Hosts translate; the wording is defined once, here. */\n key: string\n /** Fallback text for that key, so a host with no translation still says something true. */\n defaultValue: string\n /** The developer-facing line: what to change, and where. Never shown to a viewer. */\n detail: string\n }\n\nconst DEFAULT_TIMEOUT = 10000\n\n/** One marker object, so the backstop is told apart from a real rejection by identity, not by text. */\nconst TIMED_OUT = Symbol('require-custom-element: timed out')\n\n/**\n * Tag name → the specifier whose module was confirmed to have registered it.\n *\n * A clash is the one failure whose fix is in a file the complaining host does not name: you are\n * told your import lost, and finding the winner is left to grep. This module is a singleton, every\n * dock goes through it, and a passing check already knows both halves — so remember the winner and\n * a later loser can be told who it collided with.\n *\n * Only successful checks write here, so the table never names a specifier that did not register.\n */\nconst registeredBy = new Map<string, string>()\n\n/**\n * The viewer-facing wording, in one place.\n *\n * Two reasons share a key on purpose. A viewer can act on \"this part did not load\" and cannot act\n * on which export name was wrong, so splitting that line would be detail addressed to nobody.\n */\nconst VIEWER_TEXT: Record<ElementFailure, { key: string; defaultValue: string }> = {\n duplicate: {\n key: 'text.element-loaded-twice',\n defaultValue: 'This part was loaded twice and could not start.'\n },\n 'missing-from-module': {\n key: 'text.element-did-not-load',\n defaultValue: 'This part could not be loaded.'\n },\n 'export-not-found': {\n key: 'text.element-did-not-load',\n defaultValue: 'This part could not be loaded.'\n },\n 'load-failed': {\n key: 'text.element-did-not-load',\n defaultValue: 'This part could not be loaded.'\n },\n 'no-answer': {\n key: 'text.element-did-not-answer',\n defaultValue: 'This part is taking too long to load.'\n }\n}\n\nfunction failure(reason: ElementFailure, detail: string): ElementCheck {\n return { ok: false, reason, ...VIEWER_TEXT[reason], detail }\n}\n\n/** Both halves of a clash when the winner is known, and an honest gap when it is not. */\nfunction clashDetail(name: string, specifier: string, note: string): string {\n const winner = registeredBy.get(name)\n\n return (\n `<${name}> was loaded from two places. ` +\n (winner\n ? `first: ${winner} · second: ${specifier}. `\n : `second: ${specifier}; the first got there before any check ran, so it is not named here. `) +\n `Delete one of the two rather than renaming the tag — every consumer in this repo has to ` +\n `import it by the same specifier. ${note}`\n )\n}\n\n/**\n * Resolve to `{ ok: true }` when `name` is registered and the registered constructor is the one\n * `load()` brought; otherwise to the reason it is not.\n *\n * Never throws and never draws anything: the caller owns its own empty state, and only the caller\n * knows whether a missing part means a disabled corner or a dead screen.\n */\nexport async function requireCustomElement(required: RequiredElement): Promise<ElementCheck> {\n const { name, load, specifier, exportName, timeout = DEFAULT_TIMEOUT } = required\n\n let timer: ReturnType<typeof setTimeout> | undefined\n let module: Record<string, unknown>\n\n try {\n module = await Promise.race([\n load(),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(TIMED_OUT), timeout)\n })\n ])\n } catch (error) {\n if (error === TIMED_OUT) {\n return failure(\n 'no-answer',\n `importing ${specifier} for <${name}> did not settle within ${timeout}ms — the import never ` +\n `resolved or rejected, so nothing can be said about the element yet`\n )\n }\n\n /*\n * A clash throws here rather than further down: the decorator calls `customElements.define`\n * while the module is being evaluated, so the losing copy takes its whole module with it.\n *\n * Match on the error's name, not its sentence — the wording is the engine's, not the spec's.\n * The registry check is what makes the match safe: NotSupportedError only means \"this tag is\n * already taken\" when the tag is, in fact, taken.\n */\n if ((error as Error | undefined)?.name === 'NotSupportedError' && customElements.get(name)) {\n return failure('duplicate', clashDetail(name, specifier, `Its define threw where nobody caught it.`))\n }\n\n return failure(\n 'load-failed',\n `importing ${specifier} for <${name}> rejected: ${String((error as Error | undefined)?.message ?? error)}`\n )\n } finally {\n clearTimeout(timer)\n }\n\n const registered = customElements.get(name)\n const loaded = module[exportName]\n\n if (!registered) {\n return failure(\n 'missing-from-module',\n `${specifier} loaded but did not register <${name}>. That specifier does not bring this ` +\n `element — check it against the one the package documents`\n )\n }\n\n if (loaded === undefined) {\n /*\n * The identity check cannot run, and two very different things land here.\n *\n * A caller who named the wrong export is one. The other is a retry of a module that already\n * lost a clash: the bundler keeps the failed module cached with empty exports, so the second\n * import resolves with nothing in it (see the header). The tag is registered by someone in both\n * cases, so the registry cannot separate them — but the record of who registered it can.\n */\n const winner = registeredBy.get(name)\n\n if (winner !== undefined && winner !== specifier) {\n return failure(\n 'duplicate',\n clashDetail(\n name,\n specifier,\n `Its module came back empty, which is what a module that already lost a clash returns.`\n )\n )\n }\n\n return failure(\n 'export-not-found',\n `${specifier} has no export named \"${exportName}\", so <${name}> cannot be verified. ` +\n `Fix the exportName passed to requireCustomElement`\n )\n }\n\n if (registered !== loaded) {\n return failure(\n 'duplicate',\n clashDetail(\n name,\n specifier,\n `Nothing threw: this copy either guards its own define, or the specifier is a barrel whose ` +\n `re-exported module never ran.`\n )\n )\n }\n\n registeredBy.set(name, specifier)\n\n return { ok: true }\n}\n\n/** Test seam. Never call this from a screen — the table is a fact about the page, not state to reset. */\nexport function forgetRegisteredElements(): void {\n registeredBy.clear()\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
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 declare class Guarded extends HTMLElement {
|
|
8
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
12
|
+
//# sourceMappingURL=guarded-define.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guarded-define.js","sourceRoot":"","sources":["../../../test/fixtures/guarded-define.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,OAAO,OAAQ,SAAQ,WAAW;CAAG;AAE3C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,qBAAqB,CAAC,EAAE,CAAC;IAC/C,cAAc,CAAC,MAAM,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAA;AACvD,CAAC","sourcesContent":["/**\n * A hand-written component that checks the registry before defining.\n *\n * Nothing throws, the module resolves, and somebody else's constructor stays registered. Only the\n * identity comparison sees this.\n */\nexport class Guarded extends HTMLElement {}\n\nif (!customElements.get('probe-guarded-clash')) {\n customElements.define('probe-guarded-clash', Guarded)\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-registration.js","sourceRoot":"","sources":["../../../test/fixtures/no-registration.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,OAAO,eAAgB,SAAQ,WAAW;CAAG","sourcesContent":["/** Loads fine and registers nothing — the shape a stale hand copy produces. */\nexport class NeverRegistered extends HTMLElement {}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
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 declare class Registers extends HTMLElement {
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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);
|
|
12
|
+
//# sourceMappingURL=registers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registers.js","sourceRoot":"","sources":["../../../test/fixtures/registers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,OAAO,SAAU,SAAQ,WAAW;CAAG;AAE7C,cAAc,CAAC,MAAM,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAAA","sourcesContent":["/**\n * A module that registers its element while being evaluated, the way lit's decorator does.\n *\n * A real module rather than a thunk that throws, because *when* the define runs is the whole\n * point: it runs during evaluation, so a clash takes the module down with it and the caller sees a\n * rejected import rather than a bad return value. A synthetic throw would skip the module loader\n * and prove nothing about that.\n */\nexport class Registers extends HTMLElement {}\n\ncustomElements.define('probe-registers', Registers)\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
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 declare class Unguarded extends HTMLElement {
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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);
|
|
10
|
+
//# sourceMappingURL=unguarded-define.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unguarded-define.js","sourceRoot":"","sources":["../../../test/fixtures/unguarded-define.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,WAAW;CAAG;AAE7C,cAAc,CAAC,MAAM,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAA","sourcesContent":["/**\n * The losing half of a clash: defines unconditionally, exactly as `@customElement` compiles to.\n *\n * The test registers this tag first, so evaluating this module throws NotSupportedError out of the\n * loader — the shape a real duplicate arrives in.\n */\nexport class Unguarded extends HTMLElement {}\n\ncustomElements.define('probe-unguarded-clash', Unguarded)\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|