hyper-html-api 0.1.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.
@@ -0,0 +1,48 @@
1
+ export class RulesParseError extends Error {
2
+ constructor(message, originalError) {
3
+ super(message)
4
+ this.name = 'RulesParseError'
5
+ this.cause = originalError
6
+ }
7
+ }
8
+
9
+ export class UnknownRulesVersion extends Error {
10
+ constructor(version) {
11
+ super(`unknown rules version: ${version}. Library supports "1".`)
12
+ this.name = 'UnknownRulesVersion'
13
+ this.version = version
14
+ }
15
+ }
16
+
17
+ export class MaxRuleDepthExceeded extends Error {
18
+ constructor(path) {
19
+ super(`rule depth exceeded 20 at path: ${path.join('.')}`)
20
+ this.name = 'MaxRuleDepthExceeded'
21
+ this.path = path
22
+ }
23
+ }
24
+
25
+ export class ShapeMismatch extends Error {
26
+ constructor(mismatches) {
27
+ super(`shape mismatch: ${mismatches.length} field(s) failed validation`)
28
+ this.name = 'ShapeMismatch'
29
+ this.mismatches = mismatches
30
+ }
31
+ }
32
+
33
+ export class EmptyListInsert extends Error {
34
+ constructor(path) {
35
+ super(
36
+ `cannot add items to empty list at "${path.join('.')}" — no sibling to clone as template. Seed the list with a hidden item first.`,
37
+ )
38
+ this.name = 'EmptyListInsert'
39
+ this.path = path
40
+ }
41
+ }
42
+
43
+ export class UpgradeAlreadyRegistered extends Error {
44
+ constructor() {
45
+ super('upgrade transform already registered; only one registration is allowed per page.')
46
+ this.name = 'UpgradeAlreadyRegistered'
47
+ }
48
+ }
@@ -0,0 +1,70 @@
1
+ import { DOM_PROPERTIES_SET } from './dom-properties.js'
2
+ import { MaxRuleDepthExceeded } from './errors.js'
3
+
4
+ const MAX_DEPTH = 20
5
+
6
+ export function extract(adapter, root, rules) {
7
+ return extractAt(adapter, root, rules, { depth: 0, path: [] })
8
+ }
9
+
10
+ function extractAt(adapter, ctx, rule, trace) {
11
+ if (trace.depth > MAX_DEPTH) throw new MaxRuleDepthExceeded(trace.path)
12
+
13
+ if (typeof rule === 'string') return extractScalar(adapter, ctx, rule)
14
+
15
+ if (Array.isArray(rule)) {
16
+ const [selector, shape] = rule
17
+ const matches = adapter.find(ctx, selector)
18
+ return matches.map((node, i) =>
19
+ extractAt(adapter, node, shape, {
20
+ depth: trace.depth + 1,
21
+ path: [...trace.path, i],
22
+ }),
23
+ )
24
+ }
25
+
26
+ if (typeof rule === 'object' && rule !== null) {
27
+ const result = {}
28
+ for (const [key, sub] of Object.entries(rule)) {
29
+ result[key] = extractAt(adapter, ctx, sub, {
30
+ depth: trace.depth + 1,
31
+ path: [...trace.path, key],
32
+ })
33
+ }
34
+ return result
35
+ }
36
+
37
+ return null
38
+ }
39
+
40
+ function extractScalar(adapter, ctx, rule) {
41
+ if (rule.endsWith('[]')) {
42
+ const selector = rule.slice(0, -2)
43
+ return adapter.find(ctx, selector).map((n) => adapter.text(n))
44
+ }
45
+
46
+ if (rule.startsWith('@')) {
47
+ return readPropOrAttr(adapter, ctx, rule.slice(1))
48
+ }
49
+
50
+ if (rule.includes('@')) {
51
+ const [selector, name] = rule.split('@')
52
+ const matches = selector ? adapter.find(ctx, selector) : [ctx]
53
+ if (matches.length === 0) return null
54
+ return readPropOrAttr(adapter, matches[0], name)
55
+ }
56
+
57
+ if (rule === '.') return adapter.text(ctx)
58
+
59
+ const matches = adapter.find(ctx, rule)
60
+ return matches.length === 0 ? null : adapter.text(matches[0])
61
+ }
62
+
63
+ function readPropOrAttr(adapter, node, name) {
64
+ if (DOM_PROPERTIES_SET.has(name)) {
65
+ const v = adapter.prop(node, name)
66
+ return v == null ? null : String(v)
67
+ }
68
+ const v = adapter.attr(node, name)
69
+ return v ? v : null
70
+ }
@@ -0,0 +1,6 @@
1
+ export { extract } from './extract.js'
2
+ export { apply } from './apply.js'
3
+ export { findRulesIn } from './rules-tag.js'
4
+ export { parseStrict, parseRelaxed } from './rules.js'
5
+ export * as errors from './errors.js'
6
+ export { DOM_PROPERTIES, DOM_PROPERTIES_SET } from './dom-properties.js'
@@ -0,0 +1,18 @@
1
+ import { parseStrict } from './rules.js'
2
+ import { UnknownRulesVersion } from './errors.js'
3
+
4
+ const RULES_TAG_ID = 'hyper-html-api'
5
+ const SUPPORTED_VERSION = '1'
6
+
7
+ export function findRulesIn(adapter, root) {
8
+ const candidates = adapter.find(root, `script#${RULES_TAG_ID}`, { includeRulesTag: true })
9
+ if (candidates.length === 0) return null
10
+
11
+ const tagNode = candidates[0]
12
+ const version = adapter.attr(tagNode, 'data-rules-version')
13
+ if (version !== SUPPORTED_VERSION) throw new UnknownRulesVersion(version)
14
+
15
+ const body = adapter.text(tagNode)
16
+ const rules = parseStrict(body)
17
+ return { rules, tagNode }
18
+ }
@@ -0,0 +1,245 @@
1
+ import { RulesParseError } from './errors.js'
2
+
3
+ /**
4
+ * Strict JSON parser for script-tag rules bodies.
5
+ */
6
+ export function parseStrict(body) {
7
+ try {
8
+ return JSON.parse(body)
9
+ } catch (e) {
10
+ throw new RulesParseError(
11
+ 'hyper-html-api script tags require strict JSON. ' +
12
+ 'Relaxed JSON (unquoted keys, single quotes, trailing commas) is only supported in the ?data= URL parameter. ' +
13
+ `Original error: ${e.message}`,
14
+ e,
15
+ )
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Relaxed JSON parser for the ?data= URL parameter. Ported verbatim from
21
+ * the legacy hyperclay/server-lib/data-extractor.js parseExtractionRules()
22
+ * so existing URLs continue to parse identically.
23
+ */
24
+ export function parseRelaxed(queryString) {
25
+ try {
26
+ return JSON.parse(queryString)
27
+ } catch (_) {
28
+ /* fall through to tokenizer */
29
+ }
30
+
31
+ const TokenType = {
32
+ BRACE_OPEN: '{',
33
+ BRACE_CLOSE: '}',
34
+ BRACKET_OPEN: '[',
35
+ BRACKET_CLOSE: ']',
36
+ COLON: ':',
37
+ COMMA: ',',
38
+ STRING: 'STRING',
39
+ SELECTOR: 'SELECTOR',
40
+ IDENTIFIER: 'IDENTIFIER',
41
+ NUMBER: 'NUMBER',
42
+ BOOLEAN: 'BOOLEAN',
43
+ }
44
+
45
+ function tokenize(input) {
46
+ const tokens = []
47
+ let i = 0
48
+
49
+ while (i < input.length) {
50
+ const char = input[i]
51
+
52
+ if (/\s/.test(char)) {
53
+ i++
54
+ continue
55
+ }
56
+
57
+ if ('{}'.includes(char)) {
58
+ tokens.push({ type: char, value: char })
59
+ i++
60
+ continue
61
+ }
62
+
63
+ if (char === '[') {
64
+ let isAttributeSelector = false
65
+ let j = i + 1
66
+
67
+ while (j < input.length && /\s/.test(input[j])) j++
68
+
69
+ if (j < input.length && /[a-zA-Z_]/.test(input[j])) {
70
+ isAttributeSelector = true
71
+ }
72
+
73
+ if (!isAttributeSelector) {
74
+ tokens.push({ type: char, value: char })
75
+ i++
76
+ continue
77
+ }
78
+ }
79
+
80
+ if (char === ']') {
81
+ tokens.push({ type: char, value: char })
82
+ i++
83
+ continue
84
+ }
85
+
86
+ if (char === ':') {
87
+ tokens.push({ type: TokenType.COLON, value: char })
88
+ i++
89
+ continue
90
+ }
91
+
92
+ if (char === ',') {
93
+ tokens.push({ type: TokenType.COMMA, value: char })
94
+ i++
95
+ continue
96
+ }
97
+
98
+ if (char === '"' || char === "'") {
99
+ const quote = char
100
+ let j = i + 1
101
+ while (j < input.length && input[j] !== quote) {
102
+ if (input[j] === '\\') j++
103
+ j++
104
+ }
105
+ tokens.push({
106
+ type: TokenType.STRING,
107
+ value: input.substring(i + 1, j),
108
+ quoted: true,
109
+ })
110
+ i = j + 1
111
+ continue
112
+ }
113
+
114
+ let j = i
115
+ let value
116
+
117
+ while (j < input.length && !/[{},]/.test(input[j])) {
118
+ if (input[j] === ':') {
119
+ const pseudoSelectors = [
120
+ ':first',
121
+ ':last',
122
+ ':nth-child',
123
+ ':nth-of-type',
124
+ ':first-child',
125
+ ':last-child',
126
+ ':first-of-type',
127
+ ':last-of-type',
128
+ ':only-child',
129
+ ':only-of-type',
130
+ ':hover',
131
+ ':focus',
132
+ ':active',
133
+ ':visited',
134
+ ':disabled',
135
+ ':enabled',
136
+ ':checked',
137
+ ':empty',
138
+ ':root',
139
+ ':target',
140
+ ':not',
141
+ ':before',
142
+ ':after',
143
+ ':nth-last-child',
144
+ ':nth-last-of-type',
145
+ ]
146
+
147
+ let isPseudoSelector = false
148
+ for (const pseudo of pseudoSelectors) {
149
+ const pseudoName = pseudo.substring(1)
150
+ const afterColon = input.substring(j + 1, j + 1 + pseudoName.length)
151
+ if (afterColon === pseudoName) {
152
+ isPseudoSelector = true
153
+ j += pseudoName.length
154
+ break
155
+ }
156
+ }
157
+
158
+ if (!isPseudoSelector) break
159
+ } else if (input[j] === '[') {
160
+ j++
161
+ while (j < input.length && input[j] !== ']') {
162
+ if (input[j] === '"' || input[j] === "'") {
163
+ const quote = input[j]
164
+ j++
165
+ while (j < input.length && input[j] !== quote) {
166
+ if (input[j] === '\\') j++
167
+ j++
168
+ }
169
+ }
170
+ j++
171
+ }
172
+ if (j < input.length && input[j] === ']') j++
173
+ } else {
174
+ j++
175
+ }
176
+ }
177
+ value = input.substring(i, j)
178
+
179
+ let type = TokenType.IDENTIFIER
180
+
181
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
182
+ type = TokenType.NUMBER
183
+ } else if (value === 'true' || value === 'false' || value === 'null') {
184
+ type = TokenType.BOOLEAN
185
+ } else if (/^[.#@\[]|[.#@\[]| /.test(value)) {
186
+ type = TokenType.SELECTOR
187
+ }
188
+
189
+ tokens.push({ type, value, quoted: false })
190
+ i = j
191
+ }
192
+
193
+ return tokens
194
+ }
195
+
196
+ function tokensToJSON(tokens) {
197
+ let result = ''
198
+
199
+ for (let i = 0; i < tokens.length; i++) {
200
+ const token = tokens[i]
201
+
202
+ if ('{}'.includes(token.type) || '[]'.includes(token.type)) {
203
+ result += token.value
204
+ continue
205
+ }
206
+
207
+ if (token.type === TokenType.COLON) {
208
+ result += token.value
209
+ continue
210
+ }
211
+
212
+ if (token.type === TokenType.COMMA) {
213
+ result += token.value
214
+ continue
215
+ }
216
+
217
+ if (token.type === TokenType.STRING && token.quoted) {
218
+ result += `"${token.value}"`
219
+ continue
220
+ }
221
+
222
+ if (token.type === TokenType.NUMBER || token.type === TokenType.BOOLEAN) {
223
+ result += token.value
224
+ continue
225
+ }
226
+
227
+ if (token.type === TokenType.SELECTOR || token.type === TokenType.IDENTIFIER) {
228
+ result += `"${token.value}"`
229
+ continue
230
+ }
231
+
232
+ result += `"${token.value}"`
233
+ }
234
+
235
+ return result
236
+ }
237
+
238
+ try {
239
+ const tokens = tokenize(queryString)
240
+ const jsonString = tokensToJSON(tokens)
241
+ return JSON.parse(jsonString)
242
+ } catch (error) {
243
+ throw new RulesParseError('Invalid extraction rules syntax: ' + error.message, error)
244
+ }
245
+ }
@@ -0,0 +1,39 @@
1
+ import * as engineMod from './engine/index.js'
2
+ import * as upgradeMod from './upgrade/index.js'
3
+ import domAdapter from './adapters/dom.js'
4
+
5
+ const engine = {
6
+ extract: (root, rules) => engineMod.extract(domAdapter, root, rules),
7
+ apply: (root, rules, data) => engineMod.apply(domAdapter, root, rules, data),
8
+ findRulesIn: (root) => engineMod.findRulesIn(domAdapter, root),
9
+ parseStrict: engineMod.parseStrict,
10
+ parseRelaxed: engineMod.parseRelaxed,
11
+ errors: engineMod.errors,
12
+ DOM_PROPERTIES: engineMod.DOM_PROPERTIES,
13
+ }
14
+
15
+ const cms = {}
16
+
17
+ const upgrade = {
18
+ registerUpgrade: upgradeMod.registerUpgrade,
19
+ run: upgradeMod.run,
20
+ shapeMatch: upgradeMod.shapeMatch,
21
+ isHelperMode: upgradeMod.isHelperMode,
22
+ }
23
+
24
+ // Helper-mode auto-boot: when the library loads inside an iframe whose URL
25
+ // carries `?_hyperHtmlApi=upgrade-helper`, we wire up the postMessage handshake
26
+ // immediately so authors don't have to add any boot code themselves. Their
27
+ // `registerUpgrade(...)` calls (which run inline as v2's scripts execute) are
28
+ // captured by the registry before the handshake message fires.
29
+ if (typeof window !== 'undefined' && upgradeMod.isHelperMode(window.location)) {
30
+ upgradeMod.bootHelper({ win: window, doc: document })
31
+ }
32
+
33
+ // Named exports ensure the IIFE namespace exposes engine/cms/upgrade as direct
34
+ // properties on the `HyperHtmlApi` global, rather than nesting them under a
35
+ // `.default` key the way `export default` alone would.
36
+ export { engine, cms, upgrade }
37
+
38
+ const HyperHtmlApi = { engine, cms, upgrade }
39
+ export default HyperHtmlApi
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Helper-mode boot path. When the library loads in an iframe whose URL
3
+ * includes `?_hyperHtmlApi=upgrade-helper&parentOrigin=...`, it skips its
4
+ * normal idle state and instead participates in the postMessage protocol
5
+ * defined in upgrade-plan.md:
6
+ *
7
+ * 1. Once the iframe document is ready, post `hha:upgrade-ready` to
8
+ * `parentOrigin` with the v2 rules + version + whether a transform
9
+ * was registered.
10
+ * 2. Wait for the parent's `hha:upgrade-data` message carrying v1Data.
11
+ * 3. Run the registered transform (if any), shape-match against v2's
12
+ * rules, then `engine.apply` to the iframe DOM.
13
+ * 4. Serialize `documentElement.outerHTML` and post `hha:upgrade-result`
14
+ * back to the parent.
15
+ *
16
+ * Errors at any step bubble out as `hha:upgrade-error`.
17
+ */
18
+
19
+ import { findRulesIn } from '../engine/rules-tag.js'
20
+ import { extract } from '../engine/extract.js'
21
+ import { apply } from '../engine/apply.js'
22
+ import domAdapter from '../adapters/dom.js'
23
+ import { getRegisteredTransform } from './registry.js'
24
+ import { shapeMatch } from './shape-match.js'
25
+
26
+ const HELPER_QUERY = '_hyperHtmlApi'
27
+ const HELPER_VALUE = 'upgrade-helper'
28
+ const PARENT_ORIGIN_QUERY = 'parentOrigin'
29
+
30
+ export function isHelperMode(loc = typeof location !== 'undefined' ? location : null) {
31
+ if (!loc) return false
32
+ try {
33
+ const params = new URLSearchParams(loc.search)
34
+ return params.get(HELPER_QUERY) === HELPER_VALUE
35
+ } catch {
36
+ return false
37
+ }
38
+ }
39
+
40
+ export function getParentOrigin(loc = typeof location !== 'undefined' ? location : null) {
41
+ if (!loc) return null
42
+ const params = new URLSearchParams(loc.search)
43
+ return params.get(PARENT_ORIGIN_QUERY)
44
+ }
45
+
46
+ export function bootHelper({ win, doc, parentOrigin } = {}) {
47
+ win = win || (typeof window !== 'undefined' ? window : null)
48
+ doc = doc || (typeof document !== 'undefined' ? document : null)
49
+ if (!win || !doc) return
50
+ parentOrigin = parentOrigin || getParentOrigin(win.location)
51
+ if (!parentOrigin) return
52
+
53
+ const start = () => runHandshake({ win, doc, parentOrigin })
54
+
55
+ if (doc.readyState === 'loading') {
56
+ doc.addEventListener('DOMContentLoaded', start, { once: true })
57
+ } else {
58
+ start()
59
+ }
60
+ }
61
+
62
+ function runHandshake({ win, doc, parentOrigin }) {
63
+ let found
64
+ try {
65
+ found = findRulesIn(domAdapter, doc.body)
66
+ } catch (e) {
67
+ return postError(win, parentOrigin, e)
68
+ }
69
+ if (!found) {
70
+ return postError(
71
+ win,
72
+ parentOrigin,
73
+ new Error('helper-mode: no rules tag in v2 document'),
74
+ )
75
+ }
76
+ const rules = found.rules
77
+ const version = readMeta(doc, 'hyper-version')
78
+ const hasTransform = !!getRegisteredTransform()
79
+
80
+ const onMessage = (e) => {
81
+ if (e.source !== win.parent) return
82
+ if (e.origin !== parentOrigin) return
83
+ const msg = e.data
84
+ if (!msg || msg.type !== 'hha:upgrade-data') return
85
+ win.removeEventListener('message', onMessage)
86
+ try {
87
+ const result = runUpgrade({ doc, rules, v1Data: msg.v1Data })
88
+ win.parent.postMessage(
89
+ { type: 'hha:upgrade-result', html: result.html, summary: result.summary },
90
+ parentOrigin,
91
+ )
92
+ } catch (err) {
93
+ postError(win, parentOrigin, err)
94
+ }
95
+ }
96
+ win.addEventListener('message', onMessage)
97
+
98
+ win.parent.postMessage(
99
+ { type: 'hha:upgrade-ready', rules, version, hasTransform },
100
+ parentOrigin,
101
+ )
102
+ }
103
+
104
+ function runUpgrade({ doc, rules, v1Data }) {
105
+ const transform = getRegisteredTransform()
106
+ let transformed = v1Data
107
+ let didTransform = false
108
+ if (transform) {
109
+ transformed = transform(v1Data)
110
+ didTransform = true
111
+ }
112
+ const { data, summary } = shapeMatch(transformed, rules)
113
+ apply(domAdapter, doc.body, rules, data)
114
+ // Re-extract to confirm the apply landed; surface as part of summary.
115
+ const applied = extract(domAdapter, doc.body, rules)
116
+ const html =
117
+ '<!DOCTYPE html>\n' + doc.documentElement.outerHTML
118
+ return {
119
+ html,
120
+ summary: { ...summary, transformApplied: didTransform, appliedFieldCount: countLeaves(applied) },
121
+ }
122
+ }
123
+
124
+ function postError(win, parentOrigin, err) {
125
+ win.parent.postMessage(
126
+ { type: 'hha:upgrade-error', name: err?.name || 'Error', message: err?.message || String(err) },
127
+ parentOrigin,
128
+ )
129
+ }
130
+
131
+ function readMeta(doc, name) {
132
+ const el = doc.querySelector(`meta[name="${name}"]`)
133
+ return el ? el.getAttribute('content') : null
134
+ }
135
+
136
+ function countLeaves(v) {
137
+ if (v == null) return 0
138
+ if (Array.isArray(v)) return v.reduce((n, x) => n + countLeaves(x), 0)
139
+ if (typeof v === 'object') {
140
+ return Object.values(v).reduce((n, x) => n + countLeaves(x), 0)
141
+ }
142
+ return 1
143
+ }
@@ -0,0 +1,4 @@
1
+ export { registerUpgrade, getRegisteredTransform, _resetRegistryForTests } from './registry.js'
2
+ export { shapeMatch } from './shape-match.js'
3
+ export { run } from './runner.js'
4
+ export { isHelperMode, getParentOrigin, bootHelper } from './helper.js'
@@ -0,0 +1,19 @@
1
+ import { UpgradeAlreadyRegistered } from '../engine/errors.js'
2
+
3
+ let registered = null
4
+
5
+ export function registerUpgrade(fn) {
6
+ if (typeof fn !== 'function') {
7
+ throw new TypeError('registerUpgrade expects a function (v1Data) => v2Data')
8
+ }
9
+ if (registered) throw new UpgradeAlreadyRegistered()
10
+ registered = fn
11
+ }
12
+
13
+ export function getRegisteredTransform() {
14
+ return registered
15
+ }
16
+
17
+ export function _resetRegistryForTests() {
18
+ registered = null
19
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Iframe-based upgrade runner.
3
+ *
4
+ * Loads the v2 source URL in a hidden iframe with the `_hyperHtmlApi=upgrade-helper`
5
+ * query param. The helper inside the iframe handles transform + apply +
6
+ * serialize; the parent only orchestrates the postMessage handshake and
7
+ * returns the resulting HTML + summary to the caller.
8
+ *
9
+ * const { html, summary } = await run({ sourceUrl, v1Data })
10
+ *
11
+ * The caller decides what to do with `html` (e.g. POST to `/save`, render
12
+ * in a preview pane, etc.). The runner deliberately doesn't touch the
13
+ * network beyond the iframe load.
14
+ */
15
+
16
+ const DEFAULT_TIMEOUT_MS = 15000
17
+
18
+ export async function run({
19
+ sourceUrl,
20
+ v1Data,
21
+ v1Version = null,
22
+ parent = typeof window !== 'undefined' ? window : null,
23
+ timeoutMs = DEFAULT_TIMEOUT_MS,
24
+ hidden = true,
25
+ } = {}) {
26
+ if (!parent) throw new Error('upgrade.run requires a window context')
27
+ if (!sourceUrl) throw new Error('upgrade.run requires sourceUrl')
28
+
29
+ const helperUrl = buildHelperUrl(sourceUrl, parent.location.origin)
30
+ const expectedOrigin = new URL(helperUrl, parent.location.href).origin
31
+
32
+ const iframe = parent.document.createElement('iframe')
33
+ if (hidden) {
34
+ iframe.style.cssText =
35
+ 'position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;border:0;'
36
+ }
37
+ iframe.setAttribute('aria-hidden', 'true')
38
+ iframe.src = helperUrl
39
+ parent.document.body.appendChild(iframe)
40
+
41
+ let cleanup = () => {}
42
+ try {
43
+ return await new Promise((resolve, reject) => {
44
+ const timer = setTimeout(() => {
45
+ reject(new Error(`upgrade.run: helper iframe timed out after ${timeoutMs}ms`))
46
+ }, timeoutMs)
47
+
48
+ const onMessage = (e) => {
49
+ if (e.source !== iframe.contentWindow) return
50
+ if (e.origin !== expectedOrigin) return
51
+ const msg = e.data
52
+ if (!msg || typeof msg !== 'object') return
53
+
54
+ if (msg.type === 'hha:upgrade-ready') {
55
+ iframe.contentWindow.postMessage(
56
+ { type: 'hha:upgrade-data', v1Data, v1Version },
57
+ expectedOrigin,
58
+ )
59
+ return
60
+ }
61
+ if (msg.type === 'hha:upgrade-result') {
62
+ clearTimeout(timer)
63
+ resolve({ html: msg.html, summary: msg.summary })
64
+ return
65
+ }
66
+ if (msg.type === 'hha:upgrade-error') {
67
+ clearTimeout(timer)
68
+ const err = new Error(msg.message || 'helper error')
69
+ err.name = msg.name || 'UpgradeHelperError'
70
+ reject(err)
71
+ return
72
+ }
73
+ }
74
+
75
+ parent.addEventListener('message', onMessage)
76
+ cleanup = () => {
77
+ parent.removeEventListener('message', onMessage)
78
+ clearTimeout(timer)
79
+ if (iframe.parentNode) iframe.parentNode.removeChild(iframe)
80
+ }
81
+ })
82
+ } finally {
83
+ cleanup()
84
+ }
85
+ }
86
+
87
+ function buildHelperUrl(sourceUrl, parentOrigin) {
88
+ const url = new URL(sourceUrl, typeof location !== 'undefined' ? location.href : undefined)
89
+ url.searchParams.set('_hyperHtmlApi', 'upgrade-helper')
90
+ url.searchParams.set('parentOrigin', parentOrigin)
91
+ return url.toString()
92
+ }